RSSリーダーの実装
今回はメイン機能のRSSのリストを実装していく。
引っ張ってくるのはこちらのサイト↓
https://iflyer.tv/
RSSfeedの提供があるのでありがたく使わせていただく。
サイトによって使用時の注意事項や利用規約などがある場合があるので要注意。
流れとしては、
-
http
パッケージを使ってxmlファイルを取得 -
webfeed
パッケージを使ってFeedのitemに変換 - よしなに使う
class Rss {
const Rss({
required this.linkUrl,
required this.title,
required this.description,
});
final String linkUrl;
final String title;
final String description;
}
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が返ってくるというお手軽設計。
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
を使い、あとレイアウトも少し整えて実装していく。
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:(){
},
),
);
},
);
}
),
),
],
),
),
);
画像の表示高速化
画像なので伝わらないが、サムネイルの表示スピードが遅いのがかなり気になる。
https://zenn.dev/flutteruniv_dev/articles/20220615-160504-flutter-cached-network-image-test
↑こちらにかなりわかりやすく改善方法が書いてあったのでパク...参考にさせて頂く。
cached_network_image
パッケージと、
flutter_cache_manager
パッケージを使えば改善可能とのこと。
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,
);
},
);
}
}
- child: Image.network(
- width: 80,
- height: 80,
- fit: BoxFit.cover,
- result
- ),
+ child: CachedImage(
+ url: result,
+ ),
結果:かなり早くなった。
今回はここまで。