I’m new to app development. This is 2023 notes about state management in Flutter using Riverpod.
Code generation
Probably better NOT to use this at the start, it’s another layer of complexity. But it’s nice if I ever understand what’s happening.
Getting Started
OMG, it’s a bit confusing. Docs are ok and give example of use cases (nice one Remi). Docs are called “old” – maybe about to change. It’s worth get the “Official examples” to run and play with.
Making Providers
A Provider
exposes a read-only value. It exposes a read
() method.
A Notifier
can notify widgets it has changed. The widgets can ref.watch
() a Notifier
and will only rebuild if required. The ‘state’ of the Notifier is templated. For example Notifier<String>
manages a String.
A NotifierProvider
is both and an important class! It automatically and transparently caches values and allows them to be changed. You need to write a Notifier
<your type> (eg Notifier<List<Todo>>
) class, build()
the initial value (e.g. an empty list), and methods modify the state of <your type>. Then you write a NotifierProvider
like NotifierProvider
<TodosNotifier, List<Todo>>(())
which takes the Notifier class and the <your type>. Note the repeated boiler plate (hence code generation).
A StateProvider
is a special case of NotifierProvider
that is designed only for basic types (int, String). It just avoid an explicit Notifer
class like we had to make above.
FutureProvider
: good for getting unchangable results from an API (via http.get => Future). If the result has to be user changed, use AsyncNotifierProvider
instead. FutureProvider is a special case for simple uses. The widget displaying the (eg) API information can .watch
the FutureProvider
and use a helper class (ASyncResult
) to display either “waiting” or “result” or “error” nicely.
ChangeNotifierProvider
, StateNOtifierProvider
: don’t use!
Jeez, that’s complex, show me examples
Yeah, IKR. Here are some example from the “Official Examples” github:
Marvel API
marvel.dart
final Provider<MarvelRepo> repoProvider = Provider(MarvelRepo.new)
Ok, we’re making a Provider to store an instance of type MarvelRepo
. MarvelRepo
is a class with no internal state, it just exposes “getInfoFromAPI” methods. (The “.new
” bit I don’t understand. Just syntax thing. )
Then the rest of the file are just data-classes which take JSON from the API. The “getInfoFromAPI” methods return things like Future<Character
> objects.
home.dart
We see a ConsumerWidget
(recall ConsumerWidgets can access Providers via this.ref
). In its build method it gets a Future<Character>
(wrapped? in riverpod’s AsyncValue) and can easily display “loading” “error” “ready”. When in the “ready” state it returns Widgets displaying character.thumbnail.url
, images, etc.
configration.dart
Makes a dataclass Configuration
(with @freezed
not frozen!) to store credentials.
Then creates a FutureProvider<Configuration>
which returns just provides a (cached) Configuration
instance from a static file. Note we don’t need to manually cache. We don’t need to modify so can avoid the more complex AsyncNotifierProvider
.
final configurationsProvider = FutureProvider<Configuration>((_) async {
final content = json.decode(
await rootBundle.loadString('assets/configurations.json'),
) as Map<String, Object?>;
return Configuration.fromJson(content);
});
rest of marvel example
There is more complexity in this example code due to deep linking, caching and pagination. Not covered – I don’t need that (yet!).
StackOverFlow Example
question.dart
Another data-class with @freezed
called QuestionsResponse
which stores a normal List
of Question
-s. Question
dataclass stores a list of strings. This appears to be typical: the objects are modelled in data-classes. They are normal (not riverpod specific) and can be nested.
The mega-complex FutureProvider (below) is more complex due to paging. But I think its another FutureProvider
wrapped in some autodisponse stuff. Note this Provider itself uses client
which is a Provider<Dio
>. I reckon this is so it’s using a single instance of Dio (an http client).
final paginatedQuestionsProvider = FutureProvider.autoDispose
.family<QuestionsResponse, int>((ref, pageIndex) async {
final response = await ref
.watch(client)
.getUri<Map<String, Object?>>(uri, cancelToken: cancelToken);
...more...
returns QuestionResponse instance
}
Next we have a data-clas QuestionTheme
and a Provider<QuestionsTheme>
. This is interesting: it returns a const QuestionTheme
which is unchanging. So it’s like a nice way of replacing a global variable???
… ignoring the rest … advanced
user.dart
Well freak me out, it’s another data-class User
and Badge
. It’s used in Question
. Nothing riverpod-y. There’s also a custom Widget. It’s stateless: it gets final
, immutable (it’s @freezed which defaults to immutable) object to display.