Flutter でアプリを作ってみよう　＃3

### RSSリーダーの実装


今回はメイン機能のRSSのリストを実装していく。
引っ張ってくるのはこちらのサイト↓ 
https://iflyer.tv/
RSSfeedの提供があるのでありがたく使わせていただく。　
サイトによって使用時の注意事項や利用規約などがある場合があるので要注意。　
流れとしては、
1. `http`パッケージを使ってxmlファイルを取得
2. `webfeed`パッケージを使ってFeedのitemに変換
3. よしなに使う
```dart:rss_item.dart
class Rss {
  const Rss({
    required this.linkUrl,
    required this.title,
    required this.description,
  });
  final String linkUrl;
  final String title;
  final String description;
}
```
```dart:main.dart
import 'package:eve_search/rss_item.dart';
import 'dart:convert';
import 'package:webfeed_plus/webfeed_plus.dart';
import 'package:http/http.dart' as http;

class _MyHomePageState extends State<MyHomePage> {
  String _selectArea = '';
  List<Rss> onlyHiphopList = [];

  Future<List<Rss>> fetchFeed() async {
    final response = await http
      .get(Uri.parse('https://iflyer.tv/rss/events/'));

    if (response.statusCode != 200) {
      throw Exception('Failed to fetch');
    }
    final rssFeed = RssFeed.parse(utf8.decode(response.bodyBytes));
    final rssItemlist = rssFeed.items ?? <RssItem>[];
    final allCategoryList = rssItemlist
      .map(
        (item) => Rss(
          linkUrl: item.link ?? '',
          title: item.title ?? '',
          description: item.description ?? '',
        ),
      ).toList();
      //hot reloadの度にリストに重複して追加されてしまうのを防ぐため。
      onlyHiphopList = [];
      //descriptionに’Hip Hop’を含んでいるインスタンスのみonlyHiphopListに追加する
      for(int i = 0; i <= allCategoryList.length; i++){
        if(allCategoryList[i].description.contains('Hip Hop')){
          onlyHiphopList.add(allCategoryList[i]);
        }
      }
      return onlyHiphopList;
  }
```
準備完了。
### サムネイル（OGP画像）のURLを遷移先から取得する

こちらも`ogp_data_extract`という便利なパッケージがある。
引数に遷移先のURLを入れると、OGP画像のURLが返ってくるというお手軽設計。
```dart
import 'package:ogp_data_extract/ogp_data_extract.dart';

Future<String?> getOGPImageUrl(String url) async {
  final data = await OgpDataExtract.execute(url);
  return data?.image;
}

```
用意したこれらを`FutureBuilder`と`ListView.builder`を使い、あとレイアウトも少し整えて実装していく。
```dart:main.dart
Scaffold(
  appBar: AppBar(
    centerTitle: true,
    title: Text( widget.title,),
  ),
  body: SingleChildScrollView(
    child: Column(
      children: [
        Container(
          padding: const EdgeInsets.only(left: 24, top: 8, bottom: 4),
          alignment: Alignment.centerLeft,
          child: Row(
            children: [
              const Text(
                '全国',
                style: TextStyle(fontSize: 16),
              ),
              Text(_selectArea,
                style: const TextStyle(fontSize: 16),
              ),
            ],
          ),
        ),
        ElevatedButton(
          onPressed: () async{ 
            String selectArea = await showModalBottomSheet(
              context: context,
              isScrollControlled: true, 
              shape: const RoundedRectangleBorder(
                borderRadius: BorderRadius.vertical(top: Radius.circular(10)),
              ),
              builder: (BuildContext context) {
                return const PrefecturesWindow();
              },
            );
            setState(() {
              _selectArea = selectArea;
            });
          },
          child: const Text('エリアを選択する',),
        ),
        const SizedBox(height: 16,),
        SizedBox(
          height: 550,
          width: MediaQuery.of(context).size.width,
          child: FutureBuilder(
            future: fetchFeed(),
            builder: (context, snapshot) {
              return ListView.builder(
                itemCount: onlyHiphopList.length,
                itemBuilder: (context, index) {
                  return Card(
                    child: ListTile(
                      trailing: FutureBuilder<String?>(
                        future: getOGPImageUrl(onlyHiphopList[index].linkUrl),
                        builder: (context, snapshot) {
                          if(snapshot.hasError){
                            final error  = snapshot.error;
                            return Text('$error', style: const TextStyle(fontSize: 12,),);
                          }else if (snapshot.hasData) {
                            String result = snapshot.data!;
                            return Container(
                              padding: const EdgeInsets.symmetric(vertical: 4),
                              child: Image.network(
                                width: 80,
                                height: 80,
                                fit: BoxFit.cover,
                                result
                              ),
                            );
                          } else {
                              return  const SizedBox(
                                height: 20,
                                width: 20,
                                child: CircularProgressIndicator()
                              );
                          }
                        }
                      ),
                      title: Text(onlyHiphopList[index].title.toString()),
                      onTap:(){

                      },
                    ),
                  );
                },
              );
            }
          ),
        ),
      ],
    ),
  ),
);
```
するとこんな感じ
![Screenshot_20230621_223237.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3461823/735c6684-ecb3-95db-53d3-e6226d8396ac.jpeg)
### 画像の表示高速化
画像なので伝わらないが、サムネイルの表示スピードが遅いのがかなり気になる。
https://zenn.dev/flutteruniv_dev/articles/20220615-160504-flutter-cached-network-image-test
↑こちらにかなりわかりやすく改善方法が書いてあったのでパク...参考にさせて頂く。
`cached_network_image`パッケージと、
`flutter_cache_manager`パッケージを使えば改善可能とのこと。
```dart:cached_image.dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';

class CachedImage extends StatefulWidget {
  const CachedImage({
    super.key,
    required this.url,
    this.cacheManager,
    this.useCache = true,
    this.showLoading = false,
  });

  final String url;
  final CacheManager? cacheManager;
  final bool useCache;
  final bool showLoading;

  @override
  CachedImageState createState() => CachedImageState();
}

@visibleForTesting
class CachedImageState extends State<CachedImage> {
  final imageKey = GlobalKey(debugLabel: 'CachedImage');

  ImageProvider<Object>? get imageProvider => _imageProvider;
  ImageProvider<Object>? _imageProvider;

  dynamic get error => _error;
  dynamic _error;

  CacheManager get _defaultCacheManager => CacheManager(
        Config(
          'CachedImageKey',
          stalePeriod: const Duration(days: 1),
          maxNrOfCacheObjects: 20,
        ),
      );

  @override
  Widget build(BuildContext context) {
    if (!widget.useCache) {
      return Image(
        key: imageKey,
        image: NetworkImage(widget.url),
        loadingBuilder: widget.showLoading
            ? (context, child, progress) {
                if (progress == null) {
                  return child;
                }
                return const Center(
                  child: CircularProgressIndicator(),
                );
              }
            : null,
      );
    }
    return CachedNetworkImage(
      cacheManager: widget.cacheManager ?? _defaultCacheManager,
      imageUrl: widget.url,
      imageBuilder: (context, imageProvider) {
        _imageProvider = imageProvider;
        return Image(
          key: imageKey,
          image: imageProvider,
        );
      },
      placeholder: widget.showLoading
          ? (context, url) => const Center(
                child: CircularProgressIndicator(),
              )
          : null,
      errorWidget: (context, url, dynamic error) {
        _error = error;
        return Icon(
          Icons.error,
        );
      },
    );
  }
}
```
```diff_dart:main.dart
- child: Image.network(
-   width: 80,
-   height: 80,
-   fit: BoxFit.cover,
-   result
- ),
+ child: CachedImage(
+   url: result,
+ ),

```
結果：かなり早くなった。
今回はここまで。