Last active
December 1, 2020 20:25
-
-
Save DreadBoy/b2f2d516a88867c8bd0a46d5b997df2b to your computer and use it in GitHub Desktop.
Global store pattern in Flutter
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
class News extends StatelessWidget { | |
const News({Key key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: SafeArea( | |
child: StoreLoader<List<Article>>( | |
provider: articles, | |
builder: (context, articles) => ListView.builder( | |
shrinkWrap: true, | |
physics: NeverScrollableScrollPhysics(), | |
itemCount: articles.length, | |
itemBuilder: (ctx, index) => ListItem( | |
article: articles[index], | |
index: index, | |
length: articles.length, | |
), | |
), | |
), | |
), | |
); | |
} | |
} |
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
class APIProvider { | |
/// Static method to use with fire-and-forget requests. It will handle errors but | |
/// it will swallow them instead of returning them. You'll need to provide your | |
/// own context, though. In case you are in StatelessWidget, you can use High | |
/// order functions to fetch one from build method like this: | |
/// | |
/// | |
/// _onPressed(context) => () => ApiProvider.of(context)(Model.submit, Model.fromJson); | |
/// @override | |
/// Widget build(BuildContext context) { | |
/// return RaisedButton( | |
/// child: Text('submit'), | |
/// onPressed: _onPressed(context), | |
/// ); | |
/// } | |
static Future<T> Function( | |
Future<T> Function(Dio dio) fetcher, { | |
Dio dio, | |
}) of<T>(BuildContext context) => ( | |
fetcher, { | |
dio, | |
}) { | |
dio = dio ?? context.read(dioProvider); | |
return fetcher(dio).catchError((e, s) { | |
ErrorHandler.reportError(e, s); | |
return null; | |
}); | |
}; | |
} |
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
@JsonSerializable(fieldRename: FieldRename.snake) | |
class Article { | |
final String id; | |
Article(this.id); | |
factory Article.fromJson(Map<String, dynamic> json) => | |
_$ArticleFromJson(json); | |
Map<String, dynamic> toJson() => _$ArticleToJson(this); | |
static List<Article> listFromJson(List<dynamic> list) => | |
list.map((article) => _$ArticleFromJson(article)).toList(); | |
static Future<List<Article>> fetch(Dio dio, {CancelToken cancelToken}) async { | |
final response = await dio.get('/news', cancelToken: cancelToken); | |
return Article.listFromJson(response.data); | |
} | |
static Future<Article> Function( | |
Dio dio, { | |
CancelToken cancelToken, | |
}) fetchOne(String articleId) => (dio, {cancelToken}) async { | |
final response = await dio.get( | |
'/news/$articleId', | |
cancelToken: cancelToken, | |
); | |
return Article.fromJson(response.data); | |
}; | |
} | |
final article = | |
ChangeNotifierProvider.autoDispose.family<Store<Article>, String>( | |
(ref, id) => Store<Article>(ref, Article.fetchOne(id)), | |
); | |
final articles = ChangeNotifierProvider.autoDispose<Store<List<Article>>>( | |
(ref) => Store<List<Article>>(ref, Article.fetch)); |
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
typedef Fetcher<T> = Future<T> Function(Dio dio, {CancelToken cancelToken}); | |
class Store<T> extends ChangeNotifier { | |
final ProviderReference ref; | |
final Fetcher<T> fetcher; | |
T data; | |
bool loading = false; | |
dynamic error; | |
Store(this.ref, this.fetcher); | |
void load() { | |
loading = true; | |
notifyListeners(); | |
loadInternal().whenComplete(() { | |
loading = false; | |
notifyListeners(); | |
}); | |
} | |
Future<void> refresh() async { | |
await loadInternal(); | |
notifyListeners(); | |
} | |
@protected | |
Future<void> loadInternal() async { | |
try { | |
final value = await setUpDio<T>(ref)(fetcher); | |
data = value; | |
error = null; | |
} catch (e, s) { | |
ErrorHandler.reportError(e, s); | |
data = null; | |
error = e; | |
} | |
} | |
} | |
Future<T> Function(Fetcher<T> fetcher) setUpDio<T>( | |
AutoDisposeProviderReference ref) { | |
final cancelToken = CancelToken(); | |
ref.onDispose(() => cancelToken.cancel()); | |
final dio = ref.watch(dioProvider); | |
return (fetcher) async { | |
ref.maintainState = true; | |
final data = await fetcher(dio, cancelToken: cancelToken); | |
Timer(Duration(minutes: 2), () { | |
ref.maintainState = false; | |
}); | |
return data; | |
}; | |
} |
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
class StoreLoader<T> extends StatefulWidget { | |
final AutoDisposeChangeNotifierProvider<Store<T>> provider; | |
final Widget Function(BuildContext context, T data) builder; | |
final bool pullToRefresh; | |
final Widget Function() loading; | |
final Widget Function(dynamic error) error; | |
const StoreLoader({ | |
Key key, | |
@required this.provider, | |
@required this.builder, | |
this.pullToRefresh = true, | |
this.loading, | |
this.error, | |
}) : assert(provider != null), | |
assert(provider is ChangeNotifierProvider || | |
provider is AutoDisposeChangeNotifierProvider), | |
assert(builder != null), | |
assert(pullToRefresh != null), | |
super(key: key); | |
@override | |
_StoreLoaderState<T> createState() => _StoreLoaderState<T>(provider, builder); | |
} | |
class _StoreLoaderState<T> extends State<StoreLoader> { | |
AutoDisposeChangeNotifierProvider<Store<T>> provider; | |
Widget Function(BuildContext context, T data) builder; | |
/// We need to inject those in constructor otherwise generic type parameters | |
/// aren't carried over and `watch(provider)` returns `Store<dynamic>`. | |
_StoreLoaderState(this.provider, this.builder); | |
@override | |
void didUpdateWidget(StoreLoader<T> oldWidget) { | |
final widget = this.widget as StoreLoader<T>; | |
// We are closing over scope of builder function when we inject it | |
// in constructor. If we don't update it, we will always call it with | |
// original scope, possibly having stale state | |
this.builder = widget.builder; | |
if (this.provider != widget.provider) { | |
this.provider = widget.provider; | |
refresh(); | |
} | |
super.didUpdateWidget(oldWidget); | |
} | |
void refresh() { | |
WidgetsBinding.instance.addPostFrameCallback( | |
(_) { | |
final store = context.read(provider); | |
if (store.data == null) { | |
store.load(); | |
} | |
}, | |
); | |
} | |
@override | |
void initState() { | |
refresh(); | |
super.initState(); | |
} | |
Widget _error(dynamic error) => Padding( | |
padding: const EdgeInsets.only(bottom: 25), | |
child: ApiError(error), | |
); | |
Widget _loader() => Loader.material(); | |
@override | |
Widget build(BuildContext context) { | |
final consumer = Consumer( | |
builder: (context, watch, child) { | |
final store = watch(provider); | |
if (store.error != null) { | |
return (widget.error ?? _error)(store.error); | |
} | |
if (store.data != null) { | |
return builder(context, store.data); | |
} | |
return (widget.loading ?? _loader)(); | |
}, | |
); | |
if (!widget.pullToRefresh) { | |
return consumer; | |
} | |
return LayoutBuilder( | |
builder: (context, constraints) => SizedBox( | |
height: constraints.maxHeight, | |
child: RefreshIndicator( | |
onRefresh: () => context.read(provider).refresh(), | |
child: SingleChildScrollView( | |
physics: AlwaysScrollableScrollPhysics(), | |
child: consumer, | |
), | |
), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment