FlutterでQiitaのアプリを作ってみる その5

前回まではこちら

https://qiita.com/t0yger/items/c1ea368aab91c1572ccc

https://qiita.com/t0yger/items/9fe42fe04e5636ae9b3d

https://qiita.com/t0yger/items/2d65ad3bd66154fd2883

https://qiita.com/t0yger/items/1e2e5af17dd7bfb6601d

repository: 

https://github.com/t0yger/qiita-app

前回の続きでpagination対応やりたいと思います
ネイティブアプリのページネーションってfooterにボタンがある感じじゃないですよね
スクロールが下まで行ったら読み込んで追加で表示するみたいなものの方が多いイメージです
なので後者のものをやってみたい
調べるとすぐにこんなものが

https://pub.dev/packages/infinite_scroll_pagination

まさしく！早速使わせていただこう
今までresponseの型を作らず、Mapとして扱ってきたがこのタイミングでclassも作ろう
そうなるとObjectMapper的なのも欲しい
探してみると色々あるのだが、結局`getter/setter`を書かなきゃいけなかったりmapで変換したりと。
それなら自分で関数一つ実装するのと大差ない気がするので今回は自前で関数作成します
せっかくなのでファイルも分けたいと思います
`article_list_item.dart`を作成し以下を追加します
```dart
class ArticleListItem {
  final String? body;
  final String? tags;
  final String? title;
  final String? url;
  final String? profileImageUrl;

  ArticleListItem(
      this.body, this.tags, this.title, this.url, this.profileImageUrl);

  factory ArticleListItem.fromJson(Map<String, dynamic> json) =>
      ArticleListItem(
          json['body'],
          json['tags'].map((tag) => tag['name']).join(','),
          json['title'],
          json['url'],
          json['user']['profile_image_url']);
}
```
各変数をoptionalにしているのはqiitaのドキュメントにrequiredの情報がなかったんでとりあえずoptionalにしてます
factoryの中でmapとか使って変換してたり微妙ですが、今はアーキテクチャとか考えてないので一旦これで。あとで考えたいなとは思ってます

そしたら`getArticles`の中でこの型に変更し、ドキュメントにあるコントローラも定義します
```dart
final PagingController<int, ArticleListItem> _pagingController =
      PagingController(firstPageKey: 1);

  Future<void> getArticles(int pageKey) async {
    print(pageKey);
    try {
      var url = Uri.https('qiita.com', 'api/v2/items',
          {'page': '$pageKey', 'per_page': '$_perPage'});
      var response = await http.get(url);
      final newItems = jsonDecode(response.body);
      final articleListItems =
          newItems.map((item) => ArticleListItem.fromJson(item));
      final isLastPage = articleListItems.length < _perPage;
      if (isLastPage) {
        _pagingController.appendLastPage(articleListItems);
      } else {
        final nextPageKey = pageKey + 1;
        _pagingController.appendPage(articleListItems, nextPageKey);
      }
    } catch (error) {
      print(error);
      _pagingController.error = error;
    }
  }
```

あとは`ListView.separated`を`PagedListView`に置き換え、childだけ同じwidgetを使うようにします。コードは長いので割愛します。
これで実行。エラーになった...
```
type 'MappedListIterable<dynamic, dynamic>' is not a subtype of type 'List<ArticleListItem>'
```
dartのmap関数は任意の型にキャストしてあげないといけないみたい
書き方はいくつかあるらしい
```dart
// ここでList<ArticleListItem>になってると思ってた
final articleListItems =
          newItems.map((item) => ArticleListItem.fromJson(item));

// 書き方1
final articleListItems = newItems
          .map<ArticleListItem>((item) => ArticleListItem.fromJson(item))
          .toList();

// 書き方2
final articleListItems = newItems
          .map((item) => ArticleListItem.fromJson(item))
          .cast<ArticleListItem>()
          .toList();
```
他の言語のmapに慣れているので少し煩わしさを感じる
`newItems`が`dynamic`型なのが悪いのかな？少し脱線して検証
```dart
const dynamic items = [1,2,3,4];
final stringItems = items.map((i) => '$i').toList();
test(stringItems);

void test(List<String> items) {
  items.forEach((i) => print(i));
}
```
エラーが出る。これが今回の現象とほぼ同じ
```
Uncaught Error: TypeError: Instance of 'JSArray<dynamic>': type 'JSArray<dynamic>' is not a subtype of type 'List<String>'
```

`dynamic`型ではなく明示的に`List<int>`としてみる
```dart
const List<int> items = [1,2,3,4];
final stringItems = items.map((i) => '$i').toList();
test(stringItems);

void test(List<String> items) {
  items.forEach((i) => print(i));
}
```
エラー出ない！mapに型を教えなくても`List<String>`になってくれたっぽい
煩わしいわけではなくdartの勉強不足ですね。後で詳しく調べます。

一旦は書き方1を採用して再度起動
最後までスクロールすると次のページネーションを読み込むようになりました:tada: