Skip to content

Instantly share code, notes, and snippets.

@DreadBoy
Last active December 1, 2020 20:25
Show Gist options
  • Save DreadBoy/b2f2d516a88867c8bd0a46d5b997df2b to your computer and use it in GitHub Desktop.
Save DreadBoy/b2f2d516a88867c8bd0a46d5b997df2b to your computer and use it in GitHub Desktop.
Global store pattern in Flutter
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,
),
),
),
),
);
}
}
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;
});
};
}
@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));
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;
};
}
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