import 'package:antd_mobile/antd_mobile.dart'; import 'package:flutter/cupertino.dart'; import 'package:git_touch/utils/utils.dart'; import 'package:git_touch/widgets/error_reload.dart'; import 'package:git_touch/widgets/link.dart'; import 'package:git_touch/widgets/loading.dart'; class LongListPayload { LongListPayload({ required this.header, required this.totalCount, required this.cursor, required this.leadingItems, this.trailingItems, }); T header; int totalCount; String? cursor; List leadingItems; List? trailingItems; } // This is a scaffold for issue and pull request // Since the list could be very long, and some users may only want to to check trailing items // We should load leading and trailing items at first fetching, and do load more in the middle // e.g. https://github.com/reactjs/rfcs/pull/68 class LongListStatefulScaffold extends StatefulWidget { const LongListStatefulScaffold({ required this.title, this.trailingBuilder, required this.headerBuilder, required this.itemBuilder, required this.onRefresh, required this.onLoadMore, }); final Widget title; final Widget Function(T t)? trailingBuilder; final Widget Function(T t) headerBuilder; final Widget Function(K k) itemBuilder; final Future> Function() onRefresh; final Future> Function(String? cursor) onLoadMore; @override State> createState() => _LongListStatefulScaffoldState(); } class _LongListStatefulScaffoldState extends State> { late bool loading; bool loadingMore = false; String error = ''; LongListPayload? payload; @override void initState() { super.initState(); _refresh(); } Future _refresh() async { // Fimber.d('long list scaffold refresh'); setState(() { error = ''; loading = true; }); try { payload = await widget.onRefresh(); } catch (err) { error = err.toString(); rethrow; } finally { if (mounted) { setState(() { loading = false; }); } } } Future _loadMore() async { // Fimber.d('long list scaffold load more'); setState(() { loadingMore = true; }); try { final LongListPayload p = await widget.onLoadMore(payload!.cursor); payload!.totalCount = p.totalCount; payload!.cursor = p.cursor; payload!.leadingItems.addAll(p.leadingItems); } finally { if (mounted) { setState(() { loadingMore = false; }); } } } Widget _buildItem(BuildContext context, int index) { if (index % 2 == 1) { return CommonStyle.border; } final realIndex = index ~/ 2; if (realIndex < payload!.leadingItems.length) { return widget.itemBuilder(payload!.leadingItems[realIndex]); } else if (realIndex == payload!.leadingItems.length) { final count = payload!.totalCount - payload!.leadingItems.length + payload!.trailingItems!.length; return Container( padding: CommonStyle.padding, child: Center( child: LinkWidget( onTap: _loadMore, child: Container( padding: CommonStyle.padding, decoration: BoxDecoration( border: Border.all(color: AntTheme.of(context).colorText), ), child: Column( children: [ Text('$count hidden items', style: TextStyle( color: AntTheme.of(context).colorText, fontSize: 15)), const Padding(padding: EdgeInsets.only(top: 4)), loadingMore ? const CupertinoActivityIndicator() : Text( 'Load more...', style: TextStyle( color: AntTheme.of(context).colorPrimary, fontSize: 16), ), ], ), ), ), ), ); } else { return widget.itemBuilder(payload! .trailingItems![realIndex - payload!.leadingItems.length - 1]); } } int get _itemCount { var count = payload!.leadingItems.length + payload!.trailingItems!.length; if (payload!.totalCount > count) { count++; } return 2 * count; // including bottom border } Widget _buildSliver() { if (error.isNotEmpty) { return SliverToBoxAdapter( child: ErrorReload(text: error, onTap: _refresh)); } else if (loading) { // TODO: return const SliverToBoxAdapter(child: Loading(more: false)); } else { return SliverList( delegate: SliverChildBuilderDelegate(_buildItem, childCount: _itemCount), ); } } @override Widget build(BuildContext context) { final slivers = [ CupertinoSliverRefreshControl(onRefresh: _refresh) ]; if (payload != null) { slivers.add( SliverToBoxAdapter(child: widget.headerBuilder(payload!.header)), ); } slivers.add(_buildSliver()); return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: widget.title, trailing: payload == null ? null : widget.trailingBuilder!(payload!.header), ), child: SafeArea( child: CupertinoScrollbar( child: CustomScrollView(slivers: slivers), ), ), ); } }