首先
我认为大多数应用程序都使用api通讯。
当我调查是否有可能轻松生成客户时,我发现了一个名为Retrofit的库。
这次我将介绍它。
- 改造
- 改造介绍YouTube
示例应用
首先,大致确定示例应用程序的规格。
- 使用Qiita的api获取最新文章
- 在列表中显示获取的文章
- 点击文章标题以在Webview中打开文章详细信息
这一次,我们将制作一个像这样的简单应用!
Reforit如何运作
在介绍它之前,先大致了解它是如何工作的。
您可以通过查看官方自述文件和示例来查看它,但是
用抽象定义api端点。
它是一种基于此定义的文件自动生成客户端实体的机制。
自动生成的文件通常具有
(也许是g生成的?)
在
pubspec.yaml
通常的yaml定义。
*如果要修复版本,请重写任何版本。
1 2 3 4 5 6 7 8 9 | dependencies: http: any retrofit: ^1.3.4 json_annotation: ^3.0.1 dev_dependencies: retrofit_generator: any json_serializable: any build_runner: any |
api客户端摘要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // qiita_client.dart part 'qiita_client.g.dart'; // これが自動生成される実体のファイル名 // ここにbaseUrlを定義(引数で上書きできるようになってます) @RestApi(baseUrl: "https://qiita.com/api") abstract class QiitaClient { // dioの説明は割愛しますm(_ _)m // ここはまだ実体(_QiitaClient)がないのでエラーになったままです。 // 自動生成すると、qiita_client.g.dartの中に_QiitaClientができます factory QiitaClient(Dio dio, {String baseUrl}) = _QiitaClient; @GET("/v2/items") Future<List<QiitaArticle>> fetchItems( @Field("page") int page, @Field("per_page") int perPage, @Field("query") String query); } |
请求/响应数据类定义
这一次,仅定义了响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // qiita_article.dart part 'qiita_article.g.dart'; // クラスの中に独自クラスがあって展開する場合はexplicitToJson:trueにします。 // ここではQiitaUserという独自クラスがあるのでtrueにしてます。 @JsonSerializable(explicitToJson: true) class QiitaArticle { // JsonKeyでjsonの名前を定義します。同じなら省略できます。 @JsonKey(name: 'rendered_body') String renderedBody; String body; bool coediting; @JsonKey(name: 'comments_count') int commentsCount; @JsonKey(name: 'created_at') DateTime createdAt; String group; String id; @JsonKey(name: 'likes_count') int likesCount; bool private; @JsonKey(name: 'reactions_count') int reactionsCount; List<QiitaTag> tags; String title; @JsonKey(name: 'updated_at') DateTime updatedAt; String url; QiitaUser user; @JsonKey(name: 'page_views_count') int pageViewsCount; QiitaArticle({ this.renderedBody, this.body, this.coediting, this.commentsCount, this.createdAt, this.group, this.id, this.likesCount, this.private, this.reactionsCount, this.tags, this.title, this.updatedAt, this.url, this.user, this.pageViewsCount, }); } |
建议您在运行自动生成之前不要编写额外的代码(工厂,常量,吸气剂等)。
发生某些错误时未生成文件。
自动生成
文件准备好后,在终端中执行以下命令。
1 | flutter pub run build_runner build |
如果正常结束,则会弹出.g.dart。
增加了映射功能
由于它是自动生成的,因此我将添加json→class和factory。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | part 'qiita_article.g.dart'; @JsonSerializable(explicitToJson: true) class QiitaArticle { @JsonKey(name: 'rendered_body') String renderedBody; String body; bool coediting; @JsonKey(name: 'comments_count') int commentsCount; @JsonKey(name: 'created_at') DateTime createdAt; String group; String id; @JsonKey(name: 'likes_count') int likesCount; bool private; @JsonKey(name: 'reactions_count') int reactionsCount; List<QiitaTag> tags; String title; @JsonKey(name: 'updated_at') DateTime updatedAt; String url; QiitaUser user; @JsonKey(name: 'page_views_count') int pageViewsCount; QiitaArticle({ this.renderedBody, this.body, this.coediting, this.commentsCount, this.createdAt, this.group, this.id, this.likesCount, this.private, this.reactionsCount, this.tags, this.title, this.updatedAt, this.url, this.user, this.pageViewsCount, }); // ↓ 追記 factory QiitaArticle.fromJson(Map<String, dynamic> json) => _$QiitaArticleFromJson(json); Map<String, dynamic> toJson() => _$QiitaArticleToJson(this); @override String toString() => json.encode(toJson()); // ↑ 追記 } |
使用api客户端
让我们实际使用创建的客户端。
这次,我将创建一个存储库以生成一个客户端并调用它。
我还需要
状态代码,因此我决定将其转换为名为ApiResponse的类并返回它。
(这可以在客户端完成)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | class QiitaRepository { final QiitaClient _client; QiitaRepository([QiitaClient client]): // オプショナルの第2引数でbaseUrlを変更できる // QiitaClient(Dio(), "http://127.0.0.1:8081") という感じ _client = client ?? QiitaClient(Dio()) ; Future<ApiResponse> fetchArticle(int page, int perPage, String query) async { return await _client.fetchItems(page, perPage, query) .then((value) => ApiResponse(ApiResponseType.OK, value)) .catchError((e) { // エラーハンドリングについてのretrofit公式ドキュメント // https://pub.dev/documentation/retrofit/latest/ int errorCode = 0; String errorMessage = ""; switch (e.runtimeType) { case DioError: // 失敗した応答のエラーコードとメッセージを取得するサンプル // ここでエラーコードのハンドリングると良さげ final res = (e as DioError).response; if (res != null) { errorCode = res.statusCode; errorMessage = res.statusMessage; } break; default: } // ??? 省略 ??? }); } } // 共通のレスポンスクラスとして定義 // resultはdynamicにしとく。(使う側でcastする) class ApiResponse { final ApiResponseType apiStatus; final dynamic result; final String customMessage; ApiResponse(this.apiStatus, this.result, this.customMessage); } // ここは必要に応じて定義 enum ApiResponseType { OK, BadRequest, Forbidden, NotFound, MethodNotAllowed, Conflict, InternalServerError, Other, } |
尝试致电
因为这次我使用ChangeNotifier,所以我在ViewModel端编码了调用部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class HomeScreenViewModel with ChangeNotifier { QiitaRepository _qiitaRepository; List<QiitaArticle> articles = []; HomeScreenViewModel([QiitaRepository qiitaRepository]) { _qiitaRepository = qiitaRepository ?? QiitaRepository(); } Future<bool> fetchArticle() async { return _qiitaRepository.fetchArticle(1, 20, "qiita user:Qiita") .then((result) { if (result == null || result.apiStatus!= ApiResponseType.OK) { // TODO: 何かしらのエラー処理 // 画面に変更通知 notifyListeners(); return false; } // 結果を配列にadd articles.addAll(result.result); // 画面に変更通知 notifyListeners(); return true; }); } } |
屏幕侧的列表像这样简单地显示在列表中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | // ??? 省略 ??? ListView.builder( key: Key(WidgetKey.KEY_HOME_LIST_VIEW), itemBuilder: (BuildContext context, int index) { var length = context.read<HomeScreenViewModel>().articles.length -1; // 最終行まできたら if (index == length) { // 追加読み込みの関数をcall context.read<HomeScreenViewModel>().loadMore(context); // 画面にはローディング表示しておく return new Center( child: new Container( margin: const EdgeInsets.only(top: 8.0), width: 32.0, height: 32.0, child: const CircularProgressIndicator(), ), ); } else if (index > length) { // ローディング表示より先は無し return null; } // データがあるので行アイテムを作成して返却 return Container( child: rowWidget(context, index), alignment: Alignment.bottomLeft, decoration: BoxDecoration( border: Border.all(color: Colors.grey) ), ); }, ) // ??? 省略 ??? |
完成了!
在最后
这很容易,因为它会自动生成麻烦的api通信的实际部分。
由于baseUrl也可以替换,因此我认为可以轻松进行模拟。 (我还没有尝试过)
如果您正在使用它,并且有问题,我将添加它。
单击此处查看最终示例项目
*由于已对其进行了一点点修改,因此与此处描述的代码有所不同。
接下来,让我们检查单元测试,小部件测试和集成测试。