2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-07-30

目的

webfeedというツールを使って、複数のRSSフィードからコンテンツを取得、時系列順に並べて表示させるUIをFlutterFirebaseを使って実装します。

準備

image.png

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

Feed data classの実装

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

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ライブラリ内で定義されているRssItemAtomItemをそのまま利用します。その上にパーサ機能を追加したクラスを各Feed typeに対して定義しています(RssFeedItemsAtomFeedItems)。

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

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を実装します。

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

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のどこかでユーザー認証を済ませてあるという想定で行っています。ユーザー認証に関する詳細は例えば過去の記事等を参考にしてください。

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

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

// 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というListforEach内で格納しておきます。これらはList<Future>形式となりますのでFuture.waitを用いてまとめてFuture<List>形式のへ変換しておきます(feedItemListFuture)。

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

## 結果

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

image.png

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

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?