Created
June 12, 2022 17:41
-
-
Save terryl1900/47e02e32ab9260816c10997d267c5f35 to your computer and use it in GitHub Desktop.
Build Creator - Step 2 - News Example
This file contains 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'; | |
// Simple news app with infinite list of news. It shows combining creators | |
// for loading indicator and fetching data with pagination. | |
// repo.dart | |
// Pretend calling a backend service to get news. | |
const count = 10; // Item count per page. | |
Future<List<String>> fetchNews(int page) async { | |
await Future.delayed(const Duration(seconds: 1)); | |
return List.generate(count, (index) { | |
final date = DateTime.now().subtract(Duration(days: page * count + index)); | |
return '${date.toIso8601String().substring(0, 10)} is a peaceful day'; | |
}); | |
} | |
// logic.dart | |
// Hide _page in file level private variable and expose fetchMore API. | |
final _page = Creator((ref, self) => 0); | |
void fetchMore(Ref ref) => ref.update<int>(_page, (n) => n + 1); | |
// Loading indicator. | |
final loading = Creator((ref, self) => true); | |
// Hold the data, since news cannot read itself yet in this version. | |
final _news = Creator<List<String>>((ref, self) => []); | |
// News fetches next page when _page changes. | |
final news = Creator<Future<List<String>>>((ref, self) async { | |
ref.set(loading, true); | |
final next = await fetchNews(ref.watch(_page, self)); | |
final current = ref.watch(_news, null); | |
final data = [...current, ...next]; | |
ref.set(loading, false); | |
ref.set(_news, data); | |
return data; | |
}); | |
// main.dart | |
void main() { | |
runApp(CreatorGraph(child: const MyApp())); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
home: Scaffold( | |
appBar: AppBar(title: const Text('News example')), | |
body: const NewsList(), | |
), | |
); | |
} | |
} | |
class NewsList extends StatelessWidget { | |
const NewsList({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Watcher( | |
((context, ref, self) { | |
return FutureBuilder<List<String>>( | |
future: ref.watch(news, self), | |
builder: (context, snapshot) { | |
if (snapshot.data == null) { | |
return const CircularProgressIndicator(); | |
} | |
return ListView.builder( | |
itemCount: snapshot.data!.length + 1, | |
itemBuilder: ((context, index) { | |
if (index == snapshot.data!.length) { | |
// Using another Watcher here is an optional optimization, | |
// to avoid rebuild the whole list when loading indicator | |
// changes. | |
return Watcher(((context, ref, self) { | |
return TextButton( | |
onPressed: ref.watch(loading, self) | |
? null | |
: () => fetchMore(ref), | |
child: Text( | |
ref.watch(loading, self) ? 'Loading' : 'Load more'), | |
); | |
})); | |
} else { | |
return Center(child: Text(snapshot.data![index])); | |
} | |
}), | |
); | |
}); | |
}), | |
); | |
} | |
} | |
// -------------------------- Creator Library ---------------------------------- | |
/// Creator creates a stream of T. | |
class Creator<T> { | |
const Creator(this.create); | |
final T Function(Ref ref, Creator<T> self) create; | |
Element<T> _createElement(Ref ref) => Element<T>(ref, this); | |
} | |
// Element holds the state for creator. | |
class Element<T> { | |
Element(this.ref, this.creator) : state = creator.create(ref, creator); | |
final Ref ref; | |
final Creator<T> creator; | |
T state; | |
void recreate() { | |
final newState = creator.create(ref, creator); | |
if (newState != state) { | |
state = newState; | |
ref._onStateChange(creator); | |
} | |
} | |
} | |
/// Ref holds the creator states and dependencies. | |
class Ref { | |
Ref(); | |
/// Elements which hold state. | |
final Map<Creator, Element> _elements = {}; | |
/// Dependency graph. Think this as a directional graph. | |
/// A -> [B, C] means if A changes, B and C need change too. | |
final Map<Creator, Set<Creator>> _graph = {}; | |
/// Get or create an element for creator. | |
Element _element<T>(Creator creator) => | |
_elements.putIfAbsent(creator, () => creator._createElement(this)); | |
/// Add an edge creator -> watcher to the graph, then return creator's state. | |
T watch<T>(Creator<T> creator, Creator? watcher) { | |
if (watcher != null) { | |
(_graph[creator] ??= {}).add(watcher); | |
} | |
return _element<T>(creator).state; | |
} | |
/// Set state of the creator. | |
void set<T>(Creator<T> creator, T state) { | |
final element = _element<T>(creator); | |
if (state != element.state) { | |
element.state = state; | |
_onStateChange(creator); | |
} | |
} | |
/// Set state of creator using an update function. See [set]. | |
void update<T>(Creator<T> creator, T Function(T) update) => | |
set<T>(creator, update(_element(creator).state)); | |
/// Force creator to recreate its state. | |
void recreate(Creator creator) { | |
_element(creator).recreate(); | |
} | |
/// Delete the creator if it has no watcher. Also delete other creators who | |
/// loses all their watchers. | |
void dispose(Creator creator) { | |
if ((_graph[creator] ?? {}).isNotEmpty) { | |
return; // The creator is being watched by someone, cannot dispose it. | |
} | |
_elements.remove(creator); | |
_graph.remove(creator); | |
for (final c in _elements.keys.toSet()) { | |
if ((_graph[c] ?? {}).contains(creator)) { | |
_graph[c]!.remove(creator); | |
dispose(c); // Dispose c if creator is the only watcher of c. | |
} | |
} | |
} | |
/// Propagate state changes. | |
void _onStateChange(Creator creator) { | |
for (final c in _graph[creator] ?? {}) { | |
_element(c).recreate(); | |
} | |
} | |
} | |
/// CreatorGraph simply expose Ref through context. | |
class CreatorGraph extends InheritedWidget { | |
CreatorGraph({Key? key, required Widget child}) | |
: super(key: key, child: child); | |
final Ref ref = Ref(); | |
static CreatorGraph of(BuildContext context) => | |
context.dependOnInheritedWidgetOfExactType<CreatorGraph>()!; | |
@override | |
bool updateShouldNotify(CreatorGraph oldWidget) => ref != oldWidget.ref; | |
} | |
extension ContextRef on BuildContext { | |
Ref get ref => CreatorGraph.of(this).ref; | |
} | |
/// Watch creators to build a widget or to perform other action. | |
class Watcher extends StatefulWidget { | |
const Watcher(this.builder, {Key? key}) : super(key: key); | |
/// Allows watching creators to populate a widget. | |
final Widget Function(BuildContext context, Ref ref, Creator self)? builder; | |
@override | |
State<Watcher> createState() => _WatcherState(); | |
} | |
class _WatcherState extends State<Watcher> { | |
late Creator<Widget> builder; | |
late Ref ref; | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
ref = CreatorGraph.of(context).ref; // Save ref to use in dispose. | |
builder = Creator((ref, self) { | |
setState(() {}); | |
return widget.builder!(context, ref, self); | |
}); | |
} | |
@override | |
void dispose() { | |
ref.dispose(builder); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
ref.recreate(builder); | |
return ref.watch(builder, null); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment