目的
webfeed
というツールを使って、複数のRSSフィードからコンテンツを取得、時系列順に並べて表示させるUIをFlutter
とFirebase
を使って実装します。
準備
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
ライブラリ内で定義されているRssItem
とAtomItem
をそのまま利用します。その上にパーサ機能を追加したクラスを各Feed typeに対して定義しています(RssFeedItems
、AtomFeedItems
)。
続いてこの後使うため、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
を実装します。
今回はStreamBuilder
とFutureBuilder
の双方を組み合わせることでこれを実現します。
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フィードがあるという想定です。
StrmeabBuilder
のbuilder
内においては、流れてくる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
というList
にforEach
内で格納しておきます。これらはList<Future>
形式となりますのでFuture.wait
を用いてまとめてFuture<List>
形式のへ変換しておきます(feedItemListFuture
)。
最後にFutureBuilder
を使って単一のFuture
へまとめておいたfeedItemListFuture
をresolveしつつListView
WidgetにてFeedコンテンツタイトルを表示させます。このときsort
関数を使って時系列順に並び替えておきます。
## 結果
上記で実装したWidgetを適当なScaffold
Widget等にはめ込んで表示すると以下のようになります。
複数の異なったRSSフィードをFirestore
へ登録して表示させていますが、ちゃんと時系列順に並べ替えされていることが確認できました。
問題点としてはすべてのFeedをresolveして表示させているためか(?)表示まで少し時間がかかることです。