import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/widgets.dart'; import 'package:git_touch/models/theme.dart'; import 'package:git_touch/utils/utils.dart'; import 'package:provider/provider.dart'; import '../widgets/loading.dart'; import '../widgets/link.dart'; import '../widgets/error_reload.dart'; class LongListPayload { T header; int totalCount; String cursor; List leadingItems; List trailingItems; LongListPayload({ this.header, this.totalCount, this.cursor, this.leadingItems, this.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 { final Widget title; final Widget Function( T headerPayload, void Function(VoidCallback fn) setState) trailingBuilder; final Widget Function(T headerPayload) headerBuilder; final Widget Function(K itemPayload) itemBuilder; final Future> Function() onRefresh; final Future> Function(String cursor) onLoadMore; LongListStatefulScaffold({ @required this.title, this.trailingBuilder, @required this.headerBuilder, @required this.itemBuilder, @required this.onRefresh, @required this.onLoadMore, }); @override _LongListStatefulScaffoldState createState() => _LongListStatefulScaffoldState(); } class _LongListStatefulScaffoldState extends State> { 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(); throw err; } finally { if (mounted) { setState(() { loading = false; }); } } } Future _loadMore() async { // Fimber.d('long list scaffold load more'); setState(() { loadingMore = true; }); try { var _payload = await widget.onLoadMore(payload.cursor); payload.totalCount = _payload.totalCount; payload.cursor = _payload.cursor; payload.leadingItems.addAll(_payload.leadingItems); } finally { if (mounted) { setState(() { loadingMore = false; }); } } } Widget _buildItem(BuildContext context, int index) { final theme = Provider.of(context); if (index % 2 == 1) { return CommonStyle.border; } int realIndex = index ~/ 2; if (realIndex < payload.leadingItems.length) { return widget.itemBuilder(payload.leadingItems[realIndex]); } else if (realIndex == payload.leadingItems.length) { var count = payload.totalCount - payload.leadingItems.length + payload.trailingItems.length; return Container( padding: CommonStyle.padding, decoration: BoxDecoration( image: DecorationImage( image: ExactAssetImage('images/progressive-disclosure-line.png', scale: 2), repeat: ImageRepeat.repeatX, ), ), child: Center( child: Link( onTap: _loadMore, child: Container( padding: CommonStyle.padding, decoration: BoxDecoration( border: Border.all(color: theme.paletteOf(context).text), ), child: Column( children: [ Text('$count hidden items', style: TextStyle( color: theme.paletteOf(context).text, fontSize: 15)), Padding(padding: EdgeInsets.only(top: 4)), loadingMore ? CupertinoActivityIndicator() : Text( 'Load more...', style: TextStyle( color: theme.paletteOf(context).primary, fontSize: 16), ), ], ), ), ), ), ); } else { return widget.itemBuilder( payload.trailingItems[realIndex - payload.leadingItems.length - 1]); } } int get _itemCount { int 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 SliverToBoxAdapter(child: Loading(more: false)); } else { return SliverList( delegate: SliverChildBuilderDelegate(_buildItem, childCount: _itemCount), ); } } @override Widget build(BuildContext context) { switch (Provider.of(context).theme) { case AppThemeType.cupertino: List 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, setState), ), child: SafeArea( child: CupertinoScrollbar( child: CustomScrollView(slivers: slivers), ), ), ); default: return Scaffold( appBar: AppBar( title: widget.title, actions: payload == null ? null : [widget.trailingBuilder(payload.header, setState)], ), body: RefreshIndicator( onRefresh: _refresh, child: Scrollbar( child: CustomScrollView(slivers: [ if (payload != null) SliverToBoxAdapter( child: widget.headerBuilder(payload.header)), _buildSliver(), ]), ), ), ); } } }