這篇文主要是介紹在 Flutter 中如何串接 restful api ,主要是使用 Dio(意外的跟這個系列文題目切題) 這個套件以及搭配幾個處理JSON資料的套件。
基本上 Dart 有提供自己的解決方案像是內建的 HttpClient
或者是 Dart 團隊自己寫的套件 http
,其實如果只是簡單的call api 之類的行為這三者其實沒什麼差,但 dio 提供比較多好用的功能,但我自己覺得 dio 最大的好處是可以很簡單的使用,也可以封裝的很複雜。
這次就直接新建立一個專案,詳細流程就不再說明一次了。
然後安裝這次會用的套件:
dependencies:
mobx: ^2.0.4
flutter_mobx: ^2.0.2
freezed_annotation: ^0.14.3
dio: ^4.0.0
dev_dependencies:
mobx_codegen: ^2.0.3
build_runner: ^2.1.4
freezed: ^0.14.5
json_serializable: ^5.0.2
這次是採用 https://jsonplaceholder.typicode.com/ 的免費API server,但我們還需要對回傳資料經過json反序列化的處理,所以會有幾個問題。
那首先我們先來將JSON資料轉成 Dart code。
這邊我們會用https://app.quicktype.io/ 這個線上工具幫我們達成這件事情,但這個工具目前有一個小缺點,就是他還不支援 null safety dart 所以我們後續還是額外加工,當然這件事有其他解決方案,但要操作一堆套件就覺得有點麻煩,為了快速實作demo就不使用其他方法了。
我們先來實作將 /users
的資料轉成 Dart code看看。
首先先到 https://jsonplaceholder.typicode.com/users 這個頁面會發現他有一個JSON Data 就把整個都複製下來,然後貼上上面的 quicktype 裡面,
接下來幾個選項要注意:
freezed
的套件使用的。接下來就複製到我們的編輯器上:
新增一個叫做 user.dart的檔案然後貼上
// To parse this JSON data, do
//
// final users = usersFromJson(jsonString);
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:convert';
part 'users.freezed.dart';
part 'users.g.dart';
List<Users> usersFromJson(String str) =>
List<Users>.from(json.decode(str).map((x) => Users.fromJson(x)));
String usersToJson(List<Users> data) =>
json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
@freezed
abstract class Users with _$Users {
const factory Users({
int id,
String name,
String username,
String email,
Address address,
String phone,
String website,
Company company,
}) = _Users;
factory Users.fromJson(Map<String, dynamic> json) => _$UsersFromJson(json);
}
// 底下省略
// 底下省略
// 底下省略
這邊會有好幾個 abstract class
要處理,最主要的工作就是將factory
的每個type都標成nullable ,像是這樣:
const factory Users({
int? id,
String? name,
String? username,
String? email,
Address? address,
String? phone,
String? website,
Company? company,
}) = _Users;
其他 abstract class
就如法炮製,這邊的話就可以用 command + D
來選取Type 來快速編輯(但別按太快選到 factory
以外的就是了) 。
都用好後就可以使用 build_runner 來產生我們要的code了。
這邊說一下 freezed
的功用就是可以從我們的model class 來產生可以去做JSON反序列化的 code。讓我們從api server拿到 JSON字串時可以經過這個code 變成有type保障的物件。
至此我們做了這些事情:
拿到JSON資料 → 轉成 model class →經過 freezed
來產生JSON反序列化的方法。
接下來就要來真正使用 Dio
了,以下的封裝形式是我參考網路文章及別人的github的:
import 'package:dio/dio.dart';
class HttpService {
late Dio _dio;
final baseUrl = "https://jsonplaceholder.typicode.com/";
HttpService() {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
));
initializeInterceptors();
}
Future<Response> _request(String path, {required String method}) async {
Response response;
try {
response = await _dio.request(path, options: Options(method: method));
} on DioError catch (e) {
print(e.message);
throw Exception(e.message);
}
return response;
}
Future<Response> get(String path) async {
return _request(path, method: 'get');
}
initializeInterceptors() {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
print("${options.method} ${options.path}");
return handler.next(options);
},
onResponse: (response, handler) {
return handler.next(response);
},
onError: (DioError e, handler) {
return handler.next(e);
},
),
);
}
}
先來看這個 class
的 constructor
BaseOptions
就是可以設定的一些共用設定
這邊就將baseUrl設定好而已
initializeInterceptors
就是讓我們的 _dio
去初始化的「攔截器」的function
裡面會有幾個攔截點 onRequest
、 onResponse
、 onError
這邊應該看名字就知道作用了,透過攔截器我們能在這些時間點做一些操作,但目前就先寫這樣就好。
然後會看到 _request
及 get
,我這邊的規劃是get
是給外部的呼叫介面,而內部不論哪個 method都是透過 _request
進行處理,只是我目前只有先實作get
。
我們先在 widget裡宣告一個 async funtcion
Future<List<Users>> fetchData() async {
HttpService httpService = HttpService();
final response = await httpService.get('users');
final jsonStr = json.encode(response.data);
final result = usersFromJson(jsonStr);
print(result[0]);
return result;
}
我們使用了我們剛剛封裝的 dio 來進行 get request ,因為我們 httpService
有封裝了 baseUrl
所以在 get
這裡只需要傳入 'users'
就好。
之後會得到一個 response
,因為 dio 的原因所以我們還要先丟進去 json.encode
一次,後將這個 jsonStr
丟進 usersFromJson
也就是我們 users model 裡的那個方法,它最後會回傳 List<Users>
在我們真正與UI串接前我們先直接呼叫看看會怎樣。
@override
Widget build(BuildContext context) {
fetchData();
return Scaffold(
//... 省略
)
}
print
出的第一筆:
Users(id: 1, name: Leanne Graham, username: Bret, email: Sincere@april.biz, address: Address(street: Kulas Light, suite: Apt. 556, city: Gwenborough, zipcode: 92998-3874, geo: Geo(lat: -37.3159, lng: 81.1496)), phone: 1-770-736-8031 x56442, website: hildegard.org, company: Company(name: Romaguera-Crona, catchPhrase: Multi-layered client-server neural-net, bs: harness real-time e-markets))
接下來就 widget的串接了
首先會用到 FutureBuilder
這個方法,主要就是兩個參數 future
、 builder
FutureBuilder<List<Users>>(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text('loading...');
}
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return Text('${snapshot.data![0].name}');
}
}
if (snapshot.hasError) {
return const Text('error!!');
}
return const Text('loading...');
})
future
就是放置我們要處理的非同步事件那我們就放上剛剛宣告的 fetchData()
,builder
則是跟一般的 build()
類似只是這裡多一個參數 snapshot
他會有類似我們在處理 Future
時一樣可以讓我們處理各種連結狀況。
所以這邊就可以做出讀取中、完成時或發生錯誤時。
如果要測試錯誤的情況可以將 get
裡的url 隨便打就會有錯誤狀況發生。
今天的程式碼:
https://github.com/zxc469469/flutter_rest_api_playground
明天開始就要再加上 MobX,然後實作一些小功能。
參考資料: