From ba3347e31eb6cf2054b1a3e1b96a919704a02eb2 Mon Sep 17 00:00:00 2001 From: stonega Date: Sun, 31 Jan 2021 22:26:54 +0800 Subject: [PATCH] Improve search page performance. --- lib/home/pocast_discovery.dart | 140 ++++++------ lib/home/search_podcast.dart | 374 ++++++++++++++++++--------------- lib/state/search_state.dart | 13 ++ 3 files changed, 280 insertions(+), 247 deletions(-) diff --git a/lib/home/pocast_discovery.dart b/lib/home/pocast_discovery.dart index 4b9159b..cb45c87 100644 --- a/lib/home/pocast_discovery.dart +++ b/lib/home/pocast_discovery.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:line_icons/line_icons.dart'; import 'package:provider/provider.dart'; +import '../.env.dart'; import '../local_storage/key_value_storage.dart'; import '../service/search_api.dart'; import '../state/search_state.dart'; @@ -9,7 +10,6 @@ import '../type/search_api/search_genre.dart'; import '../type/search_api/searchpodcast.dart'; import '../util/extension_helper.dart'; import '../widgets/custom_widget.dart'; -import '../.env.dart'; import 'search_podcast.dart'; class DiscoveryPage extends StatefulWidget { @@ -149,6 +149,51 @@ class DiscoveryPageState extends State { ); }); + Widget _podcastCard(OnlinePodcast podcast, {VoidCallback onTap}) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), color: context.primaryColor), + width: 120, + margin: EdgeInsets.fromLTRB(10, 10, 0, 10), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(10), + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.all(4.0), + child: Column( + children: [ + Expanded( + flex: 2, + child: Center(child: PodcastAvatar(podcast)), + ), + Expanded( + flex: 1, + child: Text( + podcast.title, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.fade, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + Expanded( + flex: 1, + child: Center( + child: + SizedBox(height: 32, child: SubscribeButton(podcast)), + ), + ), + ], + ), + ), + ), + ), + ); + } + Future> _getTopPodcasts({int page}) async { final searchEngine = ListenNotesSearch(); var searchResult = await searchEngine.fetchBestPodcast( @@ -168,7 +213,7 @@ class DiscoveryPageState extends State { @override Widget build(BuildContext context) { - final searchState = context.watch(); + final searchState = context.read(); return FutureBuilder( future: _getHideDiscovery(), initialData: true, @@ -233,8 +278,12 @@ class DiscoveryPageState extends State { ) : PodcastSlideup( searchEngine: SearchEngine.listenNotes, - child: _selectedGenre == null - ? SingleChildScrollView( + child: Selector( + selector: (_, searchState) => searchState.genre, + builder: (_, genre, __) => IndexedStack( + index: genre == null ? 0 : 1, + children: [ + SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -254,75 +303,19 @@ class DiscoveryPageState extends State { return ScrollConfiguration( behavior: NoGrowBehavior(), child: ListView( + addAutomaticKeepAlives: true, scrollDirection: Axis.horizontal, children: snapshot.hasData ? snapshot.data .map((podcast) { - return Container( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular( - 10), - color: - context.primaryColor), - width: 120, - margin: EdgeInsets.fromLTRB( - 10, 10, 0, 10), - child: Material( - color: Colors.transparent, - borderRadius: - BorderRadius.circular( - 10), - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () { - searchState - .selectedPodcast = - podcast; - widget.onTap(''); - }, - child: Padding( - padding: - EdgeInsets.all(4.0), - child: Column( - children: [ - Expanded( - flex: 2, - child: Center( - child: PodcastAvatar( - podcast)), - ), - Expanded( - flex: 1, - child: Text( - podcast.title, - textAlign: - TextAlign - .center, - maxLines: 2, - overflow: - TextOverflow - .fade, - style: TextStyle( - fontWeight: - FontWeight - .bold), - ), - ), - Expanded( - flex: 1, - child: Center( - child: SizedBox( - height: 32, - child: SubscribeButton( - podcast)), - ), - ), - ], - ), - ), - ), - ), + return _podcastCard( + podcast, + onTap: () { + searchState + .selectedPodcast = + podcast; + widget.onTap(''); + }, ); }).toList() : [ @@ -349,7 +342,7 @@ class DiscoveryPageState extends State { EdgeInsets.fromLTRB(20, 0, 20, 0), onTap: () { widget.onTap(''); - setState(() => _selectedGenre = e); + searchState.setGenre = e; }, title: Text(e.name), )) @@ -369,8 +362,11 @@ class DiscoveryPageState extends State { ) ], ), - ) - : _TopPodcastList(genre: _selectedGenre), + ), + genre == null ? Center() : _TopPodcastList(genre: genre), + ], + ), + ), ), ); } diff --git a/lib/home/search_podcast.dart b/lib/home/search_podcast.dart index 58c8e75..68442fe 100644 --- a/lib/home/search_podcast.dart +++ b/lib/home/search_podcast.dart @@ -10,6 +10,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; import 'package:webfeed/webfeed.dart'; +import '../.env.dart'; import '../local_storage/key_value_storage.dart'; import '../service/search_api.dart'; import '../state/podcast_group.dart'; @@ -18,13 +19,10 @@ import '../type/search_api/searchepisodes.dart'; import '../type/search_api/searchpodcast.dart'; import '../util/extension_helper.dart'; import '../widgets/custom_widget.dart'; -import '../.env.dart'; import 'pocast_discovery.dart'; class MyHomePageDelegate extends SearchDelegate { final String searchFieldLabel; - final GlobalKey _discoveryKey = - GlobalKey(); MyHomePageDelegate({this.searchFieldLabel}) : super( searchFieldLabel: searchFieldLabel, @@ -43,15 +41,6 @@ class MyHomePageDelegate extends SearchDelegate { } } - Future _getSearchEngine() async { - final storage = KeyValueStorage(searchEngineKey); - final index = await storage.getInt(); - if (_searchEngine == null) { - _searchEngine = SearchEngine.values[index]; - } - return _searchEngine; - } - RegExp rssExp = RegExp(r'^(https?):\/\/(.*)'); Widget _invalidRss(BuildContext context) => Container( @@ -63,14 +52,15 @@ class MyHomePageDelegate extends SearchDelegate { @override void close(BuildContext context, int result) { - final selectedPodcast = context.read().selectedPodcast; + final searchState = context.read(); + final selectedPodcast = searchState.selectedPodcast; if (selectedPodcast != null) { - context.read().clearSelect(); + searchState.clearSelect(); } else { - if (_discoveryKey.currentState?.selectedGenre != null) { - _discoveryKey.currentState.backToHome(); + if (searchState.genre != null) { + searchState.clearGenre(); } else { - context.read().clearList(); + searchState.clearList(); super.close(context, result); } } @@ -79,6 +69,9 @@ class MyHomePageDelegate extends SearchDelegate { @override ThemeData appBarTheme(BuildContext context) => Theme.of(context); + @override + TextStyle get searchFieldStyle => TextStyle(fontSize: 20); + @override Widget buildLeading(BuildContext context) { return WillPopScope( @@ -103,7 +96,6 @@ class MyHomePageDelegate extends SearchDelegate { @override Widget buildSuggestions(BuildContext context) { return DiscoveryPage( - key: _discoveryKey, onTap: (history) { query = history; showResults(context); @@ -124,76 +116,25 @@ class MyHomePageDelegate extends SearchDelegate { showResults(context); }, ), - FutureBuilder( - future: _getSearchEngine(), - initialData: SearchEngine.podcastIndex, - builder: (context, snapshot) => PopupMenuButton( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - elevation: 1, - icon: SizedBox( - height: 30, - width: 30, - child: CircleAvatar( - backgroundImage: snapshot.data == SearchEngine.podcastIndex - ? AssetImage('assets/podcastindex_logo.png') - : AssetImage('assets/listennotes_logo.png'), - maxRadius: 25, - ), - ), - onSelected: (value) { - _searchEngine = value; - showSuggestions(context); - if (query != '') { - showResults(context); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: SearchEngine.podcastIndex, - child: Container( - padding: EdgeInsets.only(left: 10), - child: Row( - children: [ - Text('Podcastindex'), - Spacer(), - if (_searchEngine == SearchEngine.podcastIndex) - DotIndicator() - ], - ), - ), - ), - if(environment['apiKey'] != '') - PopupMenuItem( - value: SearchEngine.listenNotes, - child: Container( - padding: EdgeInsets.only(left: 10), - child: Row( - children: [ - Text('ListenNotes'), - Spacer(), - if (_searchEngine == SearchEngine.listenNotes) - DotIndicator() - ], - ), - ), - ), - ], - ), + _SearchPopupMenu( + onSelected: (searchEngine) { + _searchEngine = searchEngine; + showSuggestions(context); + if (query != '') { + showResults(context); + } + }, ), - SizedBox(width: 10), ]; } @override Widget buildResults(BuildContext context) { if (query.isEmpty) { - return DiscoveryPage( - key: _discoveryKey, - onTap: (history) { - query = history; - showResults(context); - }); + return DiscoveryPage(onTap: (history) { + query = history; + showResults(context); + }); } else if (rssExp.stringMatch(query) != null) { return FutureBuilder( future: _getRss(rssExp.stringMatch(query)), @@ -401,6 +342,85 @@ class _RssResultState extends State { } } +class _SearchPopupMenu extends StatefulWidget { + final ValueChanged onSelected; + _SearchPopupMenu({this.onSelected, Key key}) : super(key: key); + + @override + __SearchPopupMenuState createState() => __SearchPopupMenuState(); +} + +class __SearchPopupMenuState extends State<_SearchPopupMenu> { + SearchEngine _searchEngine; + + @override + void initState() { + super.initState(); + _searchEngine = SearchEngine.podcastIndex; + _getSearchEngine(); + } + + Future _getSearchEngine() async { + final storage = KeyValueStorage(searchEngineKey); + final index = await storage.getInt(); + setState(() => _searchEngine = SearchEngine.values[index]); + widget.onSelected(_searchEngine); + } + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + elevation: 1, + icon: SizedBox( + height: 20, + width: 20, + child: CircleAvatar( + backgroundImage: _searchEngine == SearchEngine.podcastIndex + ? AssetImage('assets/podcastindex_logo.png') + : AssetImage('assets/listennotes_logo.png'), + maxRadius: 25, + ), + ), + onSelected: (searchEngine) { + widget.onSelected(searchEngine); + setState(() { + _searchEngine = searchEngine; + }); + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: SearchEngine.podcastIndex, + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Row( + children: [ + Text('Podcastindex'), + Spacer(), + if (_searchEngine == SearchEngine.podcastIndex) DotIndicator() + ], + ), + ), + ), + if (environment['apiKey'] != '') + PopupMenuItem( + value: SearchEngine.listenNotes, + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Row( + children: [ + Text('ListenNotes'), + Spacer(), + if (_searchEngine == SearchEngine.listenNotes) DotIndicator() + ], + ), + ), + ), + ], + ); + } +} + class _ListenNotesSearch extends StatefulWidget { final String query; _ListenNotesSearch({this.query, Key key}) : super(key: key); @@ -1058,103 +1078,107 @@ class _SearchResultDetailState extends State ), ), Expanded( - child: TabBarView(children: [ - ListView( - physics: _animation.value != widget.maxHeight - ? NeverScrollableScrollPhysics() - : null, - children: [ - Html( - onLinkTap: (url) { - url.launchUrl; - }, - linkStyle: TextStyle( - color: context.accentColor, - textBaseline: TextBaseline.ideographic), - shrinkToFit: true, - data: widget.onlinePodcast.description, - padding: const EdgeInsets.only( - left: 20.0, right: 20, bottom: 20), - defaultTextStyle: TextStyle( - height: 1.8, + child: Container( + color: context.scaffoldBackgroundColor, + child: TabBarView(children: [ + ListView( + physics: _animation.value != widget.maxHeight + ? NeverScrollableScrollPhysics() + : null, + children: [ + Html( + onLinkTap: (url) { + url.launchUrl; + }, + linkStyle: TextStyle( + color: context.accentColor, + textBaseline: TextBaseline.ideographic), + shrinkToFit: true, + data: widget.onlinePodcast.description, + padding: const EdgeInsets.only( + left: 20.0, right: 20, bottom: 20), + defaultTextStyle: TextStyle( + height: 1.8, + ), ), - ), - ], - ), - FutureBuilder>( - future: _searchFuture, - builder: (context, snapshot) { - if (snapshot.hasData) { - var content = snapshot.data; - return ListView.builder( - physics: _animation.value != widget.maxHeight - ? NeverScrollableScrollPhysics() - : null, - itemCount: content.length + 1, - itemBuilder: (context, index) { - if (index == content.length) { - return Container( - padding: const EdgeInsets.only( - top: 10.0, bottom: 20.0), - alignment: Alignment.center, - child: SizedBox( - child: OutlineButton( - highlightedBorderColor: - context.accentColor, - splashColor: context.accentColor - .withOpacity(0.5), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(100))), - child: _loading - ? SizedBox( - height: 20, - width: 20, - child: - CircularProgressIndicator( - strokeWidth: 2, - )) - : Text(context.s.loadMore), - onPressed: () { - if (widget.searchEngine == - SearchEngine.listenNotes) { - _loading - ? null - : setState( - () { - _loading = true; - _searchFuture = - _getListenNotesEpisodes( - id: widget - .onlinePodcast - .id, - nextEpisodeDate: - _nextEpisdoeDate); - }, - ); - } - }), - ), + ], + ), + FutureBuilder>( + future: _searchFuture, + builder: (context, snapshot) { + if (snapshot.hasData) { + var content = snapshot.data; + return ListView.builder( + physics: _animation.value != widget.maxHeight + ? NeverScrollableScrollPhysics() + : null, + itemCount: content.length + 1, + itemBuilder: (context, index) { + if (index == content.length) { + return Container( + padding: const EdgeInsets.only( + top: 10.0, bottom: 20.0), + alignment: Alignment.center, + child: SizedBox( + child: OutlineButton( + highlightedBorderColor: + context.accentColor, + splashColor: context.accentColor + .withOpacity(0.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(100))), + child: _loading + ? SizedBox( + height: 20, + width: 20, + child: + CircularProgressIndicator( + strokeWidth: 2, + )) + : Text(context.s.loadMore), + onPressed: () { + if (widget.searchEngine == + SearchEngine.listenNotes) { + _loading + ? null + : setState( + () { + _loading = true; + _searchFuture = + _getListenNotesEpisodes( + id: widget + .onlinePodcast + .id, + nextEpisodeDate: + _nextEpisdoeDate); + }, + ); + } + }), + ), + ); + } + return ListTile( + title: Text(content[index].title), + tileColor: Colors.transparent, + subtitle: Text( + content[index].length == 0 + ? '${content[index].pubDate.toDate(context)}' + : '${content[index].length.toTime} | ' + '${content[index].pubDate.toDate(context)}', + style: TextStyle( + color: context.accentColor)), ); - } - return ListTile( - title: Text(content[index].title), - subtitle: Text( - content[index].length == 0 - ? '${content[index].pubDate.toDate(context)}' - : '${content[index].length.toTime} | ' - '${content[index].pubDate.toDate(context)}', - style: - TextStyle(color: context.accentColor)), - ); - }, + }, + ); + } + return Center( + child: CircularProgressIndicator(), ); - } - return Center( - child: CircularProgressIndicator(), - ); - }) - ]), + }) + ]), + ), ) ], ), diff --git a/lib/state/search_state.dart b/lib/state/search_state.dart index 0cf4391..fb46598 100644 --- a/lib/state/search_state.dart +++ b/lib/state/search_state.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:tsacdop/type/search_api/search_genre.dart'; import '../type/search_api/searchpodcast.dart'; class SearchState extends ChangeNotifier { @@ -8,12 +9,19 @@ class SearchState extends ChangeNotifier { bool get update => _update; OnlinePodcast _selectedPodcast; OnlinePodcast get selectedPodcast => _selectedPodcast; + Genre _genre; + Genre get genre => _genre; set selectedPodcast(OnlinePodcast podcast) { _selectedPodcast = podcast; notifyListeners(); } + set setGenre(Genre genre) { + _genre = genre; + notifyListeners(); + } + bool isSubscribed(OnlinePodcast podcast) => _subscribedList.contains(podcast); void clearSelect() { @@ -25,6 +33,11 @@ class SearchState extends ChangeNotifier { _subscribedList.clear(); } + void clearGenre(){ + _genre = null; + notifyListeners(); + } + void addPodcast(OnlinePodcast podcast) { _subscribedList.add(podcast); _update = !_update;