# Dio - 功能強大的 Http 請求庫
公司黑客松打到一半紀錄一下,<br />
因為覺得 GetXConnect 要切太多格式雷同的 providers 有點冗,<br />
畢竟服務規模如果不大的情境下其實用一個 network service 頂著就好,<br />
所以就決定用 dio 接請求了,相對簡單又跟 axios 87%像,真香。

### 優點
1. interceptor - 提供跟 axios 差不多簡易的鉤子,方便做統一擷取資料格式跟處理字定義錯誤格式。
2. 簡單的參數 - `FormData`、`Map`、含有 `MultipartFile` 的 `FormData` 都在守備範圍中。
### 踩坑
1. 定義模型 - 我之前完全沒用過 Flutter,很常遇到拿 attribute 編譯直接報錯的情況,這裡我們需要特別定義回傳的格式才行,做法我在後面會繼續說明。
2. `onResponse` 裡的 `handler.next` 的參數**必須是 `Response<dynamic>`** - 跟以前使用 JS 的 axios 認知不同,就算你想直接回傳 `response.data.data` 編譯也不會讓你過,我參考了官方的範例做法是把 `response.data` 直接改掉傳回去,因為 data 是被 Response 是先定義的合法 attribute。
3. 兩層 `xxxModel.fromJson` 的 parsing - `onResponse` 時做**請求的 parsing**,判斷後端服務客製化的 code 合法後,對**請求得到的資料做的 parsing**。
### 定義模型
下面我們用 LoginModel 當範例:
1. 安裝依賴到 pubspec.yaml
```yaml
dependecies:
json_annotation: ^4.0.0
dev_dependencies:
build_runner: ^2.0.0
json_serializable: ^4.0.0
```
2. 定義 `LoginModel`,相對位置於`lib/models/login_model.dart`。
```dart
import 'package:json_annotation/json_annotation.dart';
import 'package:my_app/models/token_model.dart';
part 'login_model.g.dart';
@JsonSerializable()
class LoginModel {
LoginModel({
required this.token,
required this.created,
});
/**
* 注意兩件事
* 1. 可以把用 JsonKey 把 data attribute 轉成駝峰式命名。
* 2. 當模型較深層,可以引用其他 model class。
*/
@JsonKey(name: 'token')
TokenModel token;
@JsonKey(name: 'created')
bool created;
factory LoginModel.fromJson(Map<String, dynamic> json) =>
_$LoginModelFromJson(json);
Map<String, dynamic> toJson() => _$LoginModelToJson(this);
}
```
3. 執行 `pub run build_runner build` 生成 `login_model.g.dart`
4. 在專案中快樂使用 `LoginModel.fromJson` 將 JSON 資料轉成 Class
之後想要新增任意 model 都可以依樣畫葫蘆,讚讚。
### 使用
以下為 Network Service 的 Class 實作:
```dart
import 'package:dio/dio.dart';
import 'package:get_storage/get_storage.dart';
import 'package:my_app/models/login_model.dart';
import 'package:my_app/models/response_wrapper_model.dart';
Dio initNetworkServiceDio() {
var dio = Dio(
BaseOptions(
baseUrl: 'BASE_URL',
connectTimeout: 30000,
receiveTimeout: 30000,
responseType: ResponseType.json,
),
);
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
GetStorage box = GetStorage();
final accessToken = box.read('access_token');
/*
登入若有提供 token 的效期的話
可以在打請求前判斷效期是否已過做重 fetch 請求的事
不過目前公司黑客松沒提供 refresh token 的 API
*/
if (accessToken is String) {
options.headers['Authorization'] = 'Bearer $accessToken';
}
handler.next(options);
},
onResponse: (Response response, handler) {
// 請求的 parsing
final body = ResponseWrapperModel.fromJson(response.data);
if (body.code != 2000) {
return handler.reject(
DioError(
requestOptions: response.requestOptions,
response: response,
type: DioErrorType.other,
error: body.msg,
),
);
}
// 改寫 response.data
response.data = body.data;
return handler.next(response);
},
onError: (DioError e, handler) {
// invalid responseStatusCode 的錯誤處理在這邊
return handler.next(e);
},
),
);
return dio;
}
class NetworkService {
static final Dio _dio = initNetworkServiceDio();
// 註冊/登入
static Future<LoginModel> login(Map form) async {
final response = await _dio.post('login/', data: form);
// 請求得到的資料的 parsing
return LoginModel.fromJson(response.data);
}
// 取得SMS驗證碼
static Future<Response<dynamic>> smsValidation(Map form) =>
_dio.post('sms-verification/', data: form);
}
```
### 雜談
浪費了不少時間在踩坑上,希望以後要接請求的朋友們不會這麼痛苦。<br />
如果有更好的做法也希望能交流一下。<br />
題外話,用手機測 SMS 登入失敗了很多次,不小心名正言順地當了一回薪水小偷,<br />
好險 PM 說沒事。

### 參考
- [Flutter - Dio](https://pub.dev/packages/dio)
- [Flutter - json_serializable](https://pub.dev/packages/json_serializable)
- [Flutter - json_annotation](https://pub.dev/packages/json_annotation)
- [Flutter - build_runner](https://dart.dev/tools/build_runner)
- [Youtube - Flutter Api Calling and JSON Parsing using Dio and Json Serializable](https://www.youtube.com/watch?v=lvRsi3PjckI)
###### tags: `Flutter`