Skip to content

Instantly share code, notes, and snippets.

@niusounds
Last active September 19, 2019 04:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save niusounds/d1127ace0b8a7bfe7d71ec6fcc2a60f2 to your computer and use it in GitHub Desktop.
Save niusounds/d1127ace0b8a7bfe7d71ec6fcc2a60f2 to your computer and use it in GitHub Desktop.
Asynchronous data management widget.
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();
});
}
}
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