[Flutter, Firebase] 簡易RSSリーダーを作る

## 目的

[`webfeed`](https://github.com/witochandra/webfeed)というツールを使って、複数のRSSフィードからコンテンツを取得、時系列順に並べて表示させるUIを`Flutter`と`Firebase`を使って実装します。

## 準備

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70838/557b36ff-fba1-e301-9395-0486b2490ead.png)

FirebaseのFireStore上で上記のようにRSS feedのURL、時刻、feed type (RSS, Atom. 後述)等を登録しておきます。UIから登録する機能を作ることができますが、今回は割愛します。

## Feed data classの実装

Dart code内にて、Feed情報を格納する`class`等を定義します。

```dart
import 'package:webfeed/webfeed.dart';

class FeedItemAndTime<T> {
  final DateTime dateTime;
  final T item;

  FeedItemAndTime(this.dateTime, this.item);
}

abstract class FeedItems {
  FeedItems(String xmlString) {
    parse(xmlString);
  }

  parse(String xmlString);
  List<FeedItemAndTime> getItems();
}

class RssFeedItems extends FeedItems {
  RssFeed _rssFeeds;

  RssFeedItems(String xmlString) : super(xmlString);

  @override
  parse(String xmlString) {
    _rssFeeds = RssFeed.parse(xmlString);
  }

  @override
  List<FeedItemAndTime<RssItem>> getItems() {
    return _rssFeeds.items
        .map((RssItem rssItem) =>
            FeedItemAndTime<RssItem>(rssItem.pubDate, rssItem))
        .toList();
  }
}

class AtomFeedItems extends FeedItems {
  AtomFeed _atomFeeds;

  AtomFeedItems(String xmlString) : super(xmlString);

  @override
  parse(String xmlString) {
    _atomFeeds = AtomFeed.parse(xmlString);
  }

  @override
  List<FeedItemAndTime<AtomItem>> getItems() {
    return _atomFeeds.items
        .map((AtomItem atomItem) =>
            FeedItemAndTime<AtomItem>(atomItem.updated, atomItem))
        .toList();
  }
}
```

あとで時系列順に並べたいので、Feedと時間を保持する`FeedItemAndTime`クラスを定義します。

Feed自体は`webfeed`ライブラリ内で定義されている`RssItem`と`AtomItem`をそのまま利用します。その上にパーサ機能を追加したクラスを各Feed typeに対して定義しています（`RssFeedItems`、`AtomFeedItems`）。

続いてこの後使うため、Feed typeとそれを判別する関数も定義しておきます。

```dart
enum FeedTypes {
  ATOM,
  RSS,
  UNKNOWN,
}

FeedTypes getFeedType(String xmlString) {
  try {
    RssFeed.parse(xmlString);
    return FeedTypes.RSS;
  } catch (e) {
    print(e);
  }

  try {
    AtomFeed.parse(xmlString);
    return FeedTypes.ATOM;
  } catch (e) {
    print(e);
  }

  throw 'Given XML string is not ATOM nor XML';
}
```

## 複数RSSフィードURLの読み出し、時系列ソート、表示Widget

さきほどFireStore上に登録しておいたRSSフィードURLからコンテンツを読み出し、時系列順にソートしてから表示させる`Widget`を実装します。

今回は`StreamBuilder`と`FutureBuilder`の双方を組み合わせることでこれを実現します。

```dart
class FeedList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final userData = Provider.of<MyUser>(context);

    return StreamBuilder<QuerySnapshot>(
        stream: FirebaseFirestore.instance
            .collection('user_data')
            .doc(userData.uid)
            .collection('feeds')
            .snapshots(),
        builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
          if (snapshot.hasError) {
            print(snapshot.error);
            return new Text('Error: ${snapshot.error}');
          }
          switch (snapshot.connectionState) {
            case ConnectionState.waiting:
              return new Text('Loading...');
            default:
              List<Future<List<FeedItemAndTime>>> feedItemFutureList = [];
              snapshot.data.docs.map((DocumentSnapshot document) {
                return getRssContent(document['uri']).then((res) {
                  final xmlData = res.data.toString();
                  FeedTypes feedType = getFeedType(xmlData);
                  switch (feedType) {
                    case FeedTypes.RSS:
                      return RssFeedItems(xmlData).getItems();
                      break;
                    case FeedTypes.ATOM:
                      return AtomFeedItems(xmlData).getItems();
                      break;
                    default:
                      throw 'Unsupported feed type $feedType';
                  }
                });
              }).forEach((Future<List<FeedItemAndTime>> futureListItem) {
                feedItemFutureList.add(futureListItem);
              });

              Future<List<List<FeedItemAndTime>>> feedItemListFuture = Future.wait(feedItemFutureList);
              return FutureBuilder<List<List<FeedItemAndTime>>>(
                  future: feedItemListFuture,
                  builder:
                      (BuildContext context, AsyncSnapshot<List<List<FeedItemAndTime>>> snapshot) {
                    if (snapshot.hasError) {
                      print(snapshot.error);
                      return new Text('Error: ${snapshot.error}');
                    }
                    switch (snapshot.connectionState) {
                      case ConnectionState.waiting:
                        return Text('Loading');
                      default:
                        List feedItemAndTimes =
                            snapshot.data.expand((x) => x).toList();
                        feedItemAndTimes
                            .sort((a, b) => b.dateTime.compareTo(a.dateTime));
                        return ListView.builder(
                          shrinkWrap: true,
                          itemCount: feedItemAndTimes.length,
                          itemBuilder: (context, index) {
                            return ListTile(
                              contentPadding: EdgeInsets.all(10.0),
                              title: Text(
                                feedItemAndTimes[index].item.title,
                              ),
                              subtitle: Text(
                                  feedItemAndTimes[index].dateTime.toString()),
                            );
                          },
                        );
                    }
                  });
          }
        });
  }
}
```

本`Widget`ではまず最初に`Provider`を使ってユーザー情報(`MyUser`）をとってきています。これは親Widgetのどこかでユーザー認証を済ませてあるという想定で行っています。ユーザー認証に関する詳細は例えば[過去の記事](https://qiita.com/festa78/items/e7a3888fd94095a484a1#flutterfirebase-google%E8%AA%8D%E8%A8%BC%E3%82%92%E7%94%A8%E3%81%84%E3%81%9F%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E8%BF%BD%E5%8A%A0)等を参考にしてください。

続いて`StreamBuilder`を作成して`Firestore`からRSSフィード情報をとってきます。`collection('feeds')`コレクション内の該当ユーザーの登録したRSSフィードがあるという想定です。

`StrmeabBuilder`の`builder`内においては、流れてくる`snapshot`情報に入っているRSSフィードURLからRSSフィードコンテンツを各Feed type事に分けて取ってきます。ここでURL情報を取ってくる時に`fetch`ではなく`getRssContent`というfirebase functions関数を使っています。これは`fetch`を使うと少なくともlocalhost環境ではCORS制限によって蹴られる事を防ぐためで、以下のように定義されています。（もっといいやり方がありそう）

```js
// Inside Javascript file which defines cloud functions.
exports.getRssContent = functions.https.onCall((data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('permission-denied', 'Auth Error');
  }

  return fetch(data)
    .then(response => {
      return response.text();
    })
    .catch((error) => { throw new functions.https.HttpsError(error) });
});
```

取ってきたRSSフィードコンテンツは`feedItemListFuture`という`List`に`forEach`内で格納しておきます。これらは`List<Future>`形式となりますので`Future.wait`を用いてまとめて`Future<List>`形式のへ変換しておきます（`feedItemListFuture`）。

最後に`FutureBuilder`を使って単一の`Future`へまとめておいた`feedItemListFuture`をresolveしつつ`ListView` WidgetにてFeedコンテンツタイトルを表示させます。このとき`sort`関数を使って時系列順に並び替えておきます。

##　結果

上記で実装したWidgetを適当な`Scaffold` Widget等にはめ込んで表示すると以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70838/e429e035-a506-b091-2613-811251252b87.png)

複数の異なったRSSフィードを`Firestore`へ登録して表示させていますが、ちゃんと時系列順に並べ替えされていることが確認できました。

問題点としてはすべてのFeedをresolveして表示させているためか（？）表示まで少し時間がかかることです。