import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/widgets.dart'; import '../providers/settings.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 LongListScaffold extends StatefulWidget { final Widget title; final Widget Function(T headerPayload) trailingBuilder; final Widget Function(T headerPayload) headerBuilder; final Widget Function(K itemPayload) itemBuilder; final Future> Function() onRefresh; final Future> Function(String cursor) onLoadMore; LongListScaffold({ @required this.title, this.trailingBuilder, @required this.headerBuilder, @required this.itemBuilder, @required this.onRefresh, @required this.onLoadMore, }); @override _LongListScaffoldState createState() => _LongListScaffoldState(); } class _LongListScaffoldState extends State> { bool loading; bool loadingMore = false; String error = ''; LongListPayload payload; @override void initState() { super.initState(); _refresh(); } Future _refresh() async { // print('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 { // print('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) { if (index % 2 == 1) { return Container( decoration: BoxDecoration( border: Border(bottom: BorderSide(color: Colors.black12)), ), ); } 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: EdgeInsets.all(16), 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: EdgeInsets.all(10), decoration: BoxDecoration( border: Border.all(color: Colors.black12), ), child: Column( children: [ Text('$count hidden items', style: TextStyle(color: Colors.black87, fontSize: 15)), Padding(padding: EdgeInsets.only(top: 4)), loadingMore ? CupertinoActivityIndicator() : Text( 'Load more...', style: TextStyle(color: Colors.blueAccent, 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 (SettingsProvider.of(context).theme) { case ThemeMap.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), ), child: SafeArea( child: CustomScrollView(slivers: slivers), ), ); default: List slivers = []; if (payload != null) { slivers.add( SliverToBoxAdapter(child: widget.headerBuilder(payload.header)), ); } slivers.add(_buildSliver()); return Scaffold( appBar: AppBar( title: widget.title, actions: payload == null ? null : [widget.trailingBuilder(payload.header)], ), body: RefreshIndicator( onRefresh: _refresh, child: CustomScrollView(slivers: slivers), ), ); } } }