Last active
September 19, 2019 04:08
-
-
Save niusounds/d1127ace0b8a7bfe7d71ec6fcc2a60f2 to your computer and use it in GitHub Desktop.
Asynchronous data management widget.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
typedef AsyncDataBuilder<T> = Widget Function(BuildContext, T); | |
typedef AsyncErrorBuilder = Widget Function(BuildContext, dynamic); | |
/// 非同期データの取得を管理するWidget。 | |
/// 初回ビルド時に非同期データを取得し、[builder]が返却するWidgetをビルドする。 | |
/// その後、必要に応じて非同期データの再取得を行い、Widgetを再構築する。 | |
/// | |
/// [refreshOnDidPopNext]をtrueにすると、[Navigator.pop]でこのWidgetが含まれる画面に戻ってきた時に再取得する。 | |
/// [refreshOnDidPopNext]をtrueにするためには、[routeObserver]も指定する必要がある。この[routeObserver]は[Navigator.observers]に含まれている必要がある。 | |
/// | |
/// [refreshOnResumed]をtrueにすると、アプリが一時停止状態から復帰した時に再取得する。 | |
/// [refreshOnResumed]と[refreshOnDidPopNext]が同時にtrueになっている場合は、現在表示中の画面でアプリが一時停止から復帰した時にのみ再取得する。 | |
class AsyncData<T> extends StatefulWidget { | |
const AsyncData({ | |
Key key, | |
@required this.asyncData, | |
@required this.builder, | |
this.errorBuilder = defaultErrorBuilder, | |
this.loadingBuilder = defaultLoadingBuilder, | |
this.refreshOnDidPopNext = false, | |
this.routeObserver, | |
this.refreshOnResumed = false, | |
}) : assert(asyncData != null), | |
assert(builder != null), | |
assert(errorBuilder != null), | |
assert(loadingBuilder != null), | |
assert(refreshOnDidPopNext != null), | |
assert(refreshOnDidPopNext ? routeObserver != null : true, | |
'When refreshOnDidPopNext is true routeObserver is required.'), | |
assert(refreshOnResumed != null), | |
super(key: key); | |
final Future<T> Function() asyncData; | |
final AsyncDataBuilder<T> builder; | |
final AsyncErrorBuilder errorBuilder; | |
final WidgetBuilder loadingBuilder; | |
final bool refreshOnDidPopNext; | |
final RouteObserver routeObserver; | |
final bool refreshOnResumed; | |
@override | |
AsyncDataState<T> createState() => AsyncDataState<T>(); | |
static Widget defaultErrorBuilder<T>(BuildContext context, T data) { | |
return Center( | |
child: Text(data.toString()), | |
); | |
} | |
static Widget defaultLoadingBuilder(BuildContext context) { | |
return const Center( | |
child: CircularProgressIndicator(), | |
); | |
} | |
static AsyncDataState<T> of<T>(BuildContext context) { | |
return context.ancestorStateOfType(TypeMatcher<AsyncDataState<T>>()); | |
} | |
} | |
class AsyncDataState<T> extends State<AsyncData<T>> | |
with RouteAware, WidgetsBindingObserver { | |
Future<T> _future; | |
@override | |
void initState() { | |
super.initState(); | |
_future = widget.asyncData(); | |
if (widget.refreshOnResumed) { | |
WidgetsBinding.instance.addObserver(this); | |
} | |
} | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
widget.routeObserver?.subscribe(this, ModalRoute.of(context)); | |
} | |
@override | |
void didUpdateWidget(AsyncData old) { | |
super.didUpdateWidget(old); | |
if (widget.routeObserver != old.routeObserver) { | |
old.routeObserver?.unsubscribe(this); | |
widget.routeObserver?.subscribe(this, ModalRoute.of(context)); | |
} | |
if (old.refreshOnResumed != widget.refreshOnResumed) { | |
if (widget.refreshOnResumed) { | |
WidgetsBinding.instance.addObserver(this); | |
} else { | |
WidgetsBinding.instance.removeObserver(this); | |
} | |
} | |
} | |
@override | |
void didPopNext() { | |
// Navigator.popで戻ってきた時に更新 | |
if (widget.refreshOnDidPopNext) { | |
refresh(); | |
} | |
} | |
@override | |
void didChangeAppLifecycleState(AppLifecycleState state) { | |
// アプリが一時停止から復帰した時 | |
if (state == AppLifecycleState.resumed && widget.refreshOnResumed) { | |
if (widget.refreshOnDidPopNext) { | |
if (ModalRoute.of(context).isCurrent) { | |
refresh(); | |
} | |
} else { | |
refresh(); | |
} | |
} | |
} | |
@override | |
void dispose() { | |
widget.routeObserver?.unsubscribe(this); | |
WidgetsBinding.instance.removeObserver(this); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return FutureBuilder<T>( | |
future: _future, | |
builder: (context, snapshot) { | |
if (snapshot.hasData) { | |
return widget.builder(context, snapshot.data); | |
} else if (snapshot.hasError) { | |
return widget.errorBuilder(context, snapshot.error); | |
} else { | |
return widget.loadingBuilder(context); | |
} | |
}, | |
); | |
} | |
/// データを再取得する。 | |
void refresh() { | |
setState(() { | |
_future = widget.asyncData(); | |
}); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'async_data.dart'; | |
void main() => runApp(MyApp()); | |
final _routeObserver = RouteObserver(); | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Flutter Demo', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
routes: { | |
'/': (context) => MyHomePage(title: 'AsyncData test'), | |
'/second': (context) => SecondPage(), | |
}, | |
navigatorObservers: [ | |
_routeObserver, | |
], | |
); | |
} | |
} | |
class MyHomePage extends StatefulWidget { | |
const MyHomePage({ | |
Key key, | |
this.title, | |
}) : super(key: key); | |
final String title; | |
@override | |
_MyHomePageState createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> { | |
final _key = GlobalKey<AsyncDataState>(debugLabel: 'AsyncData'); | |
Future<DateTime> _asyncData() async { | |
print('waiting'); | |
await Future.delayed(const Duration(seconds: 1)); | |
print('get'); | |
return DateTime.now(); | |
} | |
void _refresh() { | |
_key.currentState?.refresh(); | |
} | |
void _next() { | |
Navigator.pushNamed(context, '/second'); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text(widget.title), | |
), | |
body: Center( | |
child: AsyncData<DateTime>( | |
key: _key, | |
asyncData: _asyncData, | |
refreshOnDidPopNext: true, | |
refreshOnResumed: true, | |
routeObserver: _routeObserver, | |
builder: (context, data) { | |
return Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
Text('async data is : $data'), | |
RaisedButton( | |
onPressed: _next, | |
child: Text('next page'), | |
), | |
], | |
); | |
}, | |
), | |
), | |
floatingActionButton: FloatingActionButton( | |
onPressed: _refresh, | |
tooltip: 'Increment', | |
child: Icon(Icons.add), | |
), | |
); | |
} | |
} | |
class SecondPage extends StatefulWidget { | |
@override | |
_SecondPageState createState() => _SecondPageState(); | |
} | |
class _SecondPageState extends State<SecondPage> { | |
void _back() { | |
Navigator.maybePop(context); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('SecondPage'), | |
), | |
body: Center( | |
child: RaisedButton( | |
onPressed: _back, | |
child: Text('back'), | |
), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment