我尝试了Flutter Part7(引入了改造)


首先

我认为大多数应用程序都使用api通讯。
当我调查是否有可能轻松生成客户时,我发现了一个名为Retrofit的库。
这次我将介绍它。

  • 改造
  • 改造介绍YouTube

示例应用

首先,大致确定示例应用程序的规格。

  • 使用Qiita的api获取最新文章
  • 在列表中显示获取的文章
  • 点击文章标题以在Webview中打开文章详细信息

这一次,我们将制作一个像这样的简单应用!

Reforit如何运作

在介绍它之前,先大致了解它是如何工作的。
您可以通过查看官方自述文件和示例来查看它,但是
用抽象定义api端点。
它是一种基于此定义的文件自动生成客户端实体的机制。

自动生成的文件通常具有.g.dart和g的习惯。
(也许是g生成的?)
part子句中声明生成的文件名。

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。
image.png

增加了映射功能

由于它是自动生成的,因此我将添加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)
      ),
    );
  },
)
// ??? 省略 ???

完成了!
image.png

在最后

这很容易,因为它会自动生成麻烦的api通信的实际部分。
由于baseUrl也可以替换,因此我认为可以轻松进行模拟。 (我还没有尝试过)
如果您正在使用它,并且有问题,我将添加它。

单击此处查看最终示例项目
*由于已对其进行了一点点修改,因此与此处描述的代码有所不同。

接下来,让我们检查单元测试,小部件测试和集成测试。