diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 330e9668..49fe25db 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -132,57 +132,51 @@ class VideoCardV extends StatelessWidget { label: Utils.videoItemSemantics(videoItem), excludeSemantics: true, child: Card( - clipBehavior: Clip.hardEdge, - margin: EdgeInsets.zero, - child: GestureDetector( - onLongPress: () { - if (longPress != null) { - longPress!(); - } - }, - // onLongPressEnd: (details) { - // if (longPressEnd != null) { - // longPressEnd!(); - // } - // }, - child: InkWell( - onTap: () async => onPushDetail(heroTag), - child: Column( - children: [ - AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder(builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - src: videoItem.pic, - width: maxWidth, - height: maxHeight, - ), - ), - if (videoItem.duration > 0) - PBadge( - bottom: 6, - right: 7, - size: 'small', - type: 'gray', - text: Utils.timeFormat(videoItem.duration), - // semanticsLabel: - // '时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}', - ) - ], - ); - }), - ), - VideoContent(videoItem: videoItem) - ], + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + child: InkWell( + onTap: () async => onPushDetail(heroTag), + onLongPress: () { + if (longPress != null) { + longPress!(); + } + }, + child: Column( + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: videoItem.pic, + width: maxWidth, + height: maxHeight, + ), + ), + if (videoItem.duration > 0) + PBadge( + bottom: 6, + right: 7, + size: 'small', + type: 'gray', + text: Utils.timeFormat(videoItem.duration), + // semanticsLabel: + // '时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}', + ) + ], + ); + }), ), - ), - )), + VideoContent(videoItem: videoItem) + ], + ), + ), + ), ), if (videoItem.goto == 'av') Positioned( diff --git a/lib/http/loading_state.dart b/lib/http/loading_state.dart new file mode 100644 index 00000000..8423329b --- /dev/null +++ b/lib/http/loading_state.dart @@ -0,0 +1,26 @@ +abstract class LoadingState { + const LoadingState(); + + factory LoadingState.loading() = Loading; + factory LoadingState.empty() = Empty; + factory LoadingState.success(T response) = Success; + factory LoadingState.error(String errMsg) = Error; +} + +class Loading extends LoadingState { + const Loading(); +} + +class Empty extends LoadingState { + const Empty(); +} + +class Success extends LoadingState { + final T response; + const Success(this.response); +} + +class Error extends LoadingState { + final String errMsg; + const Error(this.errMsg); +} diff --git a/lib/http/video.dart b/lib/http/video.dart index d7f72c16..78b866c0 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1,6 +1,6 @@ import 'dart:developer'; +import 'package:PiliPalaX/http/loading_state.dart'; import 'package:dio/dio.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:hive/hive.dart'; import '../common/constants.dart'; @@ -34,7 +34,8 @@ class VideoHttp { static Box userInfoCache = GStorage.userInfo; // 首页推荐视频 - static Future rcmdVideoList({required int ps, required int freshIdx}) async { + static Future rcmdVideoList( + {required int ps, required int freshIdx}) async { var res = await Request().get( Api.recommendListWeb, data: { @@ -49,10 +50,8 @@ class VideoHttp { ); if (res.data['code'] == 0) { List list = []; - List blackMidsList = localCache - .get(LocalCacheKey.blackMidsList, defaultValue: [-1]) - .map((e) => e as int) - .toList(); + List blackMidsList = + localCache.get(LocalCacheKey.blackMidsList, defaultValue: []); for (var i in res.data['data']['item']) { //过滤掉live与ad,以及拉黑用户 if (i['goto'] == 'av' && @@ -64,14 +63,18 @@ class VideoHttp { } } } - return {'status': true, 'data': list}; + if (list.isNotEmpty) { + return LoadingState.success(list); + } else { + return LoadingState.empty(); + } } else { - return {'status': false, 'data': [], 'msg': res.data['message']}; + return LoadingState.error(res.data['message']); } } // 添加额外的loginState变量模拟未登录状态 - static Future rcmdVideoListApp( + static Future rcmdVideoListApp( {bool loginStatus = true, required int freshIdx}) async { var data = { 'access_key': loginStatus @@ -138,10 +141,8 @@ class VideoHttp { ); if (res.data['code'] == 0) { List list = []; - List blackMidsList = localCache - .get(LocalCacheKey.blackMidsList, defaultValue: [-1]) - .map((e) => e as int) - .toList(); + List blackMidsList = + localCache.get(LocalCacheKey.blackMidsList, defaultValue: []); for (var i in res.data['data']['items']) { // 屏蔽推广和拉黑用户 if (i['card_goto'] != 'ad_av' && @@ -156,36 +157,41 @@ class VideoHttp { } } } - return {'status': true, 'data': list}; + if (list.isNotEmpty) { + return LoadingState.success(list); + } else { + return LoadingState.empty(); + } } else { - return {'status': false, 'data': [], 'msg': res.data['message']}; + return LoadingState.error(res.data['message']); } } // 最热视频 - static Future hotVideoList({required int pn, required int ps}) async { - try { - var res = await Request().get( - Api.hotList, - data: {'pn': pn, 'ps': ps}, - ); - if (res.data['code'] == 0) { - List list = []; - List blackMidsList = localCache - .get(LocalCacheKey.blackMidsList, defaultValue: [-1]) - .map((e) => e as int) - .toList(); - for (var i in res.data['data']['list']) { - if (!blackMidsList.contains(i['owner']['mid'])) { - list.add(HotVideoItemModel.fromJson(i)); - } + static Future hotVideoList( + {required int pn, required int ps}) async { + var res = await Request().get( + Api.hotList, + data: {'pn': pn, 'ps': ps}, + ); + if (res.data['code'] == 0) { + List list = []; + List blackMidsList = localCache + .get(LocalCacheKey.blackMidsList, defaultValue: [-1]) + .map((e) => e as int) + .toList(); + for (var i in res.data['data']['list']) { + if (!blackMidsList.contains(i['owner']['mid'])) { + list.add(HotVideoItemModel.fromJson(i)); } - return {'status': true, 'data': list}; - } else { - return {'status': false, 'data': [], 'msg': res.data['message']}; } - } catch (err) { - return {'status': false, 'data': [], 'msg': err}; + if (list.isNotEmpty) { + return LoadingState.success(list); + } else { + return LoadingState.empty(); + } + } else { + return LoadingState.error(res.data['message']); } } diff --git a/lib/pages/common/common_controller.dart b/lib/pages/common/common_controller.dart new file mode 100644 index 00000000..5cc4a087 --- /dev/null +++ b/lib/pages/common/common_controller.dart @@ -0,0 +1,63 @@ +import 'package:PiliPalaX/http/loading_state.dart'; +import 'package:PiliPalaX/utils/extension.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; + +abstract class CommonController extends GetxController { + final ScrollController scrollController = ScrollController(); + + int currentPage = 1; + bool isLoading = false; + Rx loadingState = LoadingState.loading().obs; + + Future customGetData(); + + List? handleResponse(List currentList, List dataList) { + return null; + } + + void handleSuccess(List currentList, List dataList) {} + + Future queryData([bool isRefresh = true]) async { + if (isLoading) return; + isLoading = true; + LoadingState response = await customGetData(); + if (response is Success) { + currentPage++; + List currentList = loadingState.value is Success + ? (loadingState.value as Success).response + : []; + List? handleList = handleResponse(currentList, response.response); + loadingState.value = isRefresh + ? handleList != null + ? LoadingState.success(handleList) + : response + : LoadingState.success(currentList + response.response); + handleSuccess(currentList, response.response); + } else { + if (isRefresh) { + loadingState.value = response; + } + } + isLoading = false; + } + + Future onRefresh() async { + currentPage = 1; + await queryData(); + } + + Future onLoadMore() async { + await queryData(false); + } + + void animateToTop() { + scrollController.animToTop(); + } + + @override + void onClose() { + scrollController.dispose(); + super.onClose(); + } +} diff --git a/lib/pages/hot/controller.dart b/lib/pages/hot/controller.dart index 5d426e50..f82dece0 100644 --- a/lib/pages/hot/controller.dart +++ b/lib/pages/hot/controller.dart @@ -1,55 +1,21 @@ -import 'package:PiliPalaX/utils/extension.dart'; -import 'package:get/get.dart'; +import 'package:PiliPalaX/http/loading_state.dart'; +import 'package:PiliPalaX/pages/common/common_controller.dart'; import 'package:flutter/material.dart'; import 'package:PiliPalaX/http/video.dart'; -import 'package:PiliPalaX/models/model_hot_video_item.dart'; -class HotController extends GetxController { - final ScrollController scrollController = ScrollController(); +class HotController extends CommonController { final int _count = 20; - int _currentPage = 1; - RxList videoList = [].obs; - bool isLoadingMore = false; - bool flag = false; List popupDialog = []; - // 获取推荐 - Future queryHotFeed(type) async { - if (type != 'onLoad') { - _currentPage = 1; - } - var res = await VideoHttp.hotVideoList( - pn: _currentPage, - ps: _count, - ); - if (res['status']) { - if (type == 'init') { - videoList.value = res['data']; - } else if (type == 'onRefresh') { - // videoList.insertAll(0, res['data']); - videoList.value = res['data']; - } else if (type == 'onLoad') { - videoList.addAll(res['data']); - } - _currentPage += 1; - if (_currentPage == 2) queryHotFeed('onLoad'); - } - isLoadingMore = false; - return res; + @override + void onInit() { + super.onInit(); + queryData(); } - // 下拉刷新 - Future onRefresh() async { - await queryHotFeed('onRefresh'); - } - - // 上拉加载 - Future onLoad() async { - await queryHotFeed('onLoad'); - } - - // 返回顶部 - void animateToTop() { - scrollController.animToTop(); - } + @override + Future customGetData() => VideoHttp.hotVideoList( + pn: currentPage, + ps: _count, + ); } diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index 76a1623a..e42dcefc 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:PiliPalaX/http/loading_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; @@ -23,8 +24,6 @@ class HotPage extends StatefulWidget { class _HotPageState extends State with AutomaticKeepAliveClientMixin { final HotController _hotController = Get.put(HotController()); - List videoList = []; - Future? _futureBuilderFuture; @override bool get wantKeepAlive => true; @@ -32,7 +31,6 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { @override void initState() { super.initState(); - _futureBuilderFuture = _hotController.queryHotFeed('init'); StreamController mainStream = Get.find().bottomBarStream; StreamController searchBarStream = @@ -41,10 +39,7 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { () { if (_hotController.scrollController.position.pixels >= _hotController.scrollController.position.maxScrollExtent - 200) { - if (!_hotController.isLoadingMore) { - _hotController.isLoadingMore = true; - _hotController.onLoad(); - } + _hotController.onLoadMore(); } final ScrollDirection direction = @@ -63,7 +58,6 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { @override void dispose() { _hotController.scrollController.removeListener(() {}); - _hotController.scrollController.dispose(); super.dispose(); } @@ -80,69 +74,29 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { slivers: [ SliverPadding( // 单列布局 EdgeInsets.zero - padding: const EdgeInsets.fromLTRB(StyleString.safeSpace, - StyleString.safeSpace - 5, StyleString.safeSpace, 0), - sliver: FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data as Map; - if (data['status']) { - return Obx( - () => SliverGrid( - gridDelegate: SliverGridDelegateWithExtentAndRatio( - mainAxisSpacing: StyleString.safeSpace, - crossAxisSpacing: StyleString.safeSpace, - maxCrossAxisExtent: Grid.maxRowWidth * 2, - childAspectRatio: StyleString.aspectRatio * 2.4, - mainAxisExtent: 0), - delegate: SliverChildBuilderDelegate((context, index) { - return VideoCardH( - videoItem: _hotController.videoList[index], - showPubdate: true, - longPress: () { - _hotController.popupDialog.add(_createPopupDialog( - _hotController.videoList[index])); - Overlay.of(context) - .insert(_hotController.popupDialog.last!); - }, - longPressEnd: _removePopupDialog, - ); - }, childCount: _hotController.videoList.length), - ), - ); - } else { - return HttpError( - errMsg: data['msg'], - fn: () { - setState(() { - _futureBuilderFuture = - _hotController.queryHotFeed('init'); - }); - }, - ); - } - } else { - // 骨架屏 - return SliverGrid( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - mainAxisSpacing: StyleString.cardSpace, - crossAxisSpacing: StyleString.cardSpace, - maxCrossAxisExtent: Grid.maxRowWidth * 2, - childAspectRatio: StyleString.aspectRatio * 2.4), - delegate: SliverChildBuilderDelegate((context, index) { - return const VideoCardHSkeleton(); - }, childCount: 10), - ); - } - }, + padding: EdgeInsets.fromLTRB( + StyleString.safeSpace, + StyleString.safeSpace - 5, + StyleString.safeSpace, + MediaQuery.of(context).padding.bottom + 10, + ), + sliver: Obx( + () => _hotController.loadingState.value is Loading + ? _buildSkeleton() + : _hotController.loadingState.value is Success + ? _buildBody(_hotController.loadingState.value as Success) + : HttpError( + errMsg: _hotController.loadingState.value is Error + ? (_hotController.loadingState.value as Error) + .errMsg + : '没有相关数据', + fn: () { + _hotController.loadingState.value = + LoadingState.loading(); + _hotController.onRefresh(); + }), ), ), - SliverToBoxAdapter( - child: SizedBox( - height: MediaQuery.of(context).padding.bottom + 10, - ), - ) ], ), ); @@ -161,4 +115,48 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { ), ); } + + Widget _buildSkeleton() { + return SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + mainAxisSpacing: StyleString.cardSpace, + crossAxisSpacing: StyleString.cardSpace, + maxCrossAxisExtent: Grid.maxRowWidth * 2, + childAspectRatio: StyleString.aspectRatio * 2.4, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoCardHSkeleton(); + }, + childCount: 10, + ), + ); + } + + Widget _buildBody(Success loadingState) { + return SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: StyleString.safeSpace, + crossAxisSpacing: StyleString.safeSpace, + maxCrossAxisExtent: Grid.maxRowWidth * 2, + childAspectRatio: StyleString.aspectRatio * 2.4, + mainAxisExtent: 0, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + return VideoCardH( + videoItem: loadingState.response[index], + showPubdate: true, + longPress: () { + _hotController.popupDialog + .add(_createPopupDialog(loadingState.response[index])); + Overlay.of(context).insert(_hotController.popupDialog.last!); + }, + longPressEnd: _removePopupDialog, + ); + }, + childCount: loadingState.response.length, + ), + ); + } } diff --git a/lib/pages/rcmd/controller.dart b/lib/pages/rcmd/controller.dart index cf6e71c9..2eff050b 100644 --- a/lib/pages/rcmd/controller.dart +++ b/lib/pages/rcmd/controller.dart @@ -1,104 +1,60 @@ -import 'package:PiliPalaX/utils/extension.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; -import 'package:hive/hive.dart'; -import 'package:PiliPalaX/http/video.dart'; +import 'package:PiliPalaX/http/loading_state.dart'; import 'package:PiliPalaX/models/home/rcmd/result.dart'; -import 'package:PiliPalaX/models/model_rec_video_item.dart'; +import 'package:PiliPalaX/pages/common/common_controller.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:PiliPalaX/http/video.dart'; import 'package:PiliPalaX/utils/storage.dart'; -class RcmdController extends GetxController { - final ScrollController scrollController = ScrollController(); - int _currentPage = 0; - // RxList appVideoList = [].obs; - // RxList webVideoList = [].obs; - List popupDialog = []; - Box setting = GStorage.setting; - RxInt crossAxisCount = 2.obs; +class RcmdController extends CommonController { late bool enableSaveLastData; late String defaultRcmdType = 'web'; - late RxList videoList; + List popupDialog = []; @override void onInit() { super.onInit(); - enableSaveLastData = - setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false); - defaultRcmdType = - setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web'); - if (defaultRcmdType == 'web') { - videoList = [].obs; - } else { - videoList = [].obs; + enableSaveLastData = GStorage.setting + .get(SettingBoxKey.enableSaveLastData, defaultValue: false); + defaultRcmdType = GStorage.setting + .get(SettingBoxKey.defaultRcmdType, defaultValue: 'web'); + + currentPage = 0; + queryData(); + } + + @override + Future customGetData() { + return defaultRcmdType == 'app' || defaultRcmdType == 'notLogin' + ? VideoHttp.rcmdVideoListApp( + loginStatus: defaultRcmdType != 'notLogin', + freshIdx: currentPage, + ) + : VideoHttp.rcmdVideoList( + freshIdx: currentPage, + ps: 20, + ); + } + + @override + List? handleResponse(List currentList, List dataList) { + return currentPage == 1 && enableSaveLastData + ? dataList + + (currentList.isEmpty ? [] : currentList) + : null; + } + + @override + void handleSuccess(List currentList, List dataList) { + if (dataList.length > 1 && currentList.length < 24) { + Future.delayed(const Duration(milliseconds: 300), () { + if (currentList.length < 24) queryData(false); + }); } } - // 获取推荐 - Future queryRcmdFeed(type) async { - if (type == 'onRefresh') { - _currentPage = 0; - } - late final Map res; - switch (defaultRcmdType) { - case 'app': - case 'notLogin': - res = await VideoHttp.rcmdVideoListApp( - loginStatus: defaultRcmdType != 'notLogin', - freshIdx: _currentPage, - ); - break; - default: //'web' - res = await VideoHttp.rcmdVideoList( - freshIdx: _currentPage, - ps: 20, - ); - } - if (res['status']) { - if (type == 'init') { - if (videoList.isNotEmpty) { - videoList.addAll(res['data']); - } else { - videoList.value = res['data']; - } - } else if (type == 'onRefresh') { - if (enableSaveLastData) { - videoList.insertAll(0, res['data']); - } else { - videoList.value = res['data']; - } - } else if (type == 'onLoad') { - videoList.addAll(res['data']); - } - _currentPage += 1; - // 若videoList数量太小,可能会影响翻页,此时再次请求 - // 为避免请求到的数据太少时还在反复请求,要求本次返回数据大于1条才触发 - if (res['data'].length > 1 && videoList.length < 24) { - Future.delayed(const Duration(milliseconds: 300), () { - if (videoList.length < 24) queryRcmdFeed('onLoad'); - }); - } - if (res['data'].length < 5) { - SmartDialog.showToast("仅请求到${res['data'].length}条"); - } - } else { - SmartDialog.showToast("${res['msg']},请尝试(重新)登录"); - } - return res; - } - - // 下拉刷新 + @override Future onRefresh() async { - queryRcmdFeed('onRefresh'); - } - - // 上拉加载 - Future onLoad() async { - queryRcmdFeed('onLoad'); - } - - // 返回顶部 - void animateToTop() { - scrollController.animToTop(); + currentPage = 0; + await queryData(); } } diff --git a/lib/pages/rcmd/view.dart b/lib/pages/rcmd/view.dart index e4f18ea8..66c252a1 100644 --- a/lib/pages/rcmd/view.dart +++ b/lib/pages/rcmd/view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:PiliPalaX/http/loading_state.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -25,7 +26,6 @@ class RcmdPage extends StatefulWidget { class _RcmdPageState extends State with AutomaticKeepAliveClientMixin { final RcmdController _rcmdController = Get.put(RcmdController()); - late Future _futureBuilderFuture; @override bool get wantKeepAlive => true; @@ -33,23 +33,21 @@ class _RcmdPageState extends State @override void initState() { super.initState(); - _futureBuilderFuture = _rcmdController.queryRcmdFeed('init'); - ScrollController scrollController = _rcmdController.scrollController; StreamController mainStream = Get.find().bottomBarStream; StreamController searchBarStream = Get.find().searchBarStream; - scrollController.addListener( + _rcmdController.scrollController.addListener( () { - if (scrollController.position.pixels >= - scrollController.position.maxScrollExtent - 200) { + if (_rcmdController.scrollController.position.pixels >= + _rcmdController.scrollController.position.maxScrollExtent - 200) { EasyThrottle.throttle( 'my-throttler', const Duration(milliseconds: 200), () { - _rcmdController.onLoad(); + _rcmdController.onLoadMore(); }); } final ScrollDirection direction = - scrollController.position.userScrollDirection; + _rcmdController.scrollController.position.userScrollDirection; if (direction == ScrollDirection.forward) { mainStream.add(true); searchBarStream.add(true); @@ -64,7 +62,6 @@ class _RcmdPageState extends State @override void dispose() { _rcmdController.scrollController.removeListener(() {}); - _rcmdController.scrollController.dispose(); super.dispose(); } @@ -81,44 +78,27 @@ class _RcmdPageState extends State child: RefreshIndicator( onRefresh: () async { await _rcmdController.onRefresh(); - await Future.delayed(const Duration(milliseconds: 300)); }, child: CustomScrollView( controller: _rcmdController.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( - padding: - const EdgeInsets.fromLTRB(0, StyleString.cardSpace, 0, 0), - sliver: FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.data != null) { - Map data = snapshot.data as Map; - if (data['status']) { - return Obx( - () => contentGrid( - _rcmdController, - _rcmdController.videoList.isEmpty - ? [] - : _rcmdController.videoList), - ); - } else { - return HttpError( - errMsg: data == null ? "" : data['msg'], + padding: const EdgeInsets.only(top: StyleString.cardSpace), + sliver: Obx( + () => _rcmdController.loadingState.value is Loading || + _rcmdController.loadingState.value is Success + ? contentGrid(_rcmdController.loadingState.value) + : HttpError( + errMsg: _rcmdController.loadingState.value is Error + ? (_rcmdController.loadingState.value as Error) + .errMsg + : '没有相关数据', fn: () { - setState(() { - _futureBuilderFuture = - _rcmdController.queryRcmdFeed('init'); - }); - }, - ); - } - } else { - return contentGrid(_rcmdController, []); - } - }, + _rcmdController.loadingState.value = + LoadingState.loading(); + _rcmdController.onRefresh(); + }), ), ), ], @@ -141,7 +121,7 @@ class _RcmdPageState extends State ); } - Widget contentGrid(ctr, videoList) { + Widget contentGrid(LoadingState loadingState) { return SliverGrid( gridDelegate: SliverGridDelegateWithExtentAndRatio( // 行间距 @@ -155,12 +135,12 @@ class _RcmdPageState extends State ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { - return videoList!.isNotEmpty + return loadingState is Success ? VideoCardV( - videoItem: videoList[index], + videoItem: loadingState.response[index], longPress: () { _rcmdController.popupDialog - .add(_createPopupDialog(videoList[index])); + .add(_createPopupDialog(loadingState.response[index])); Overlay.of(context) .insert(_rcmdController.popupDialog.last!); }, @@ -168,7 +148,7 @@ class _RcmdPageState extends State ) : const VideoCardVSkeleton(); }, - childCount: videoList!.isNotEmpty ? videoList!.length : 10, + childCount: loadingState is Success ? loadingState.response.length : 10, ), ); } diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart index f794a3b2..041d733d 100644 --- a/lib/utils/extension.dart +++ b/lib/utils/extension.dart @@ -13,3 +13,31 @@ extension ScrollControllerExt on ScrollController { duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); } } + +extension ListExt on List? { + bool get isNullOrEmpty => this == null || this!.isEmpty; + + T? getOrNull(int index) { + if (isNullOrEmpty) { + return null; + } + return this![index]; + } + + bool eq(List? other) { + if (this == null) { + return other == null; + } + if (other == null || this!.length != other.length) { + return false; + } + for (int index = 0; index < this!.length; index += 1) { + if (this![index] != other[index]) { + return false; + } + } + return true; + } + + bool ne(List? other) => !eq(other); +}