Publish to my blog (weekly)
-
Flutter: The power of small and reusable widgets
- Every time I build a new feature or page in Flutter, I always ask myself which logical UI components are needed, and build them as custom widgets.
- One easy way to identify logical UI components, is to see if they are used more than once.
-
-
Singletons in Flutter: How to Avoid Them and What to do Instead
- late
- all global variables are lazy-loaded by default (and this is also true for static class variables). This means that they are only initialized when they are first used. On the other hand, local variables are initialized as soon as they are declared, unless they are declared as
late
. - final getIt = GetIt.instance; getIt.registerLazySingleton<FirebaseAuthRepository>
-
-
Flutter Riverpod: How to Register a Listener during App Startup
- / 1. Create a ProviderContainer final container = ProviderContainer(); // 2. Use it to read the provider container.read(dynamicLinksServiceProvider);
- // 3. Pass the container to an UncontrolledProviderScope and run the app
- UncontrolledProviderScope
- container: container,
- But in this case, we can ignore return value as we don't need it. Besides, we can't call any methods on it as the only public method is the constructor (which starts the listener).
-
-
Riverpod Data Caching and Providers Lifecycle: Full Guide
- ConsumerWidget
- final counter = ref.watch(counterStateProvider);
- ref.read(counterStateProvider.notifier).state++,
- ProviderScope
- And that's because all Riverpod providers are lazy-loaded.
- as a regular Provider
- PostsRepository
- as a FutureProvider
- Future<List<Post>>
- as a FutureProvider.family
- Future<Post>
- , int postId
- ConsumerWidget
- final postsAsync = ref.watch(fetchPostsProvider);
- postsAsync.when
- ConsumerWidget
- required this.postId
- final postsAsync = ref.watch(fetchPostProvider(postId));
- postsAsync.when
- print('init: fetchPost($postId)');
- ref.onDispose(() => print('dispose: fetchPost($postId)'));
- print('dio: fetchPost($postId)');
- flutter: dispose: fetchPost(1) <-- navigate back
- flutter: dispose: fetchPost(2) <-- navigate back
- flutter: dispose: fetchPost(1) <-- navigate back
- @Riverpod(keepAlive: true)
- @Riverpod(keepAlive: true)
- // get the [KeepAliveLink] final link = ref.keepAlive();
- // start a 30 second timer timer = Timer(const Duration(seconds: 30), () { // dispose on timeout link.close(); });
- right before the provider is destroyed
- when the last listener of the provider is removed
- when a provider is listened again after it was paused
- whenever a new listener is added to the provider
- whenever a new listener is removed from the provider
- NotifierProvider
- StreamProvider
- SharedPreferences
- FirebaseAuth
-
-
Side Effects in Flutter: What they are and how to avoid them
-
- mutating state
- executing asynchronous code
- // this *is* a side effect setState(() => _counter++);
- // this is *not* a side effect onPressed: () => setState(() => _counter++),
- But the second call to
setState()
is ok because it happens inside theonPressed
callback, and this is only called when we tap on the button - independently from thebuild()
method. - // this *is* a side effect counter.value++;
- // this is *not* a side effect onPressed: () => counter.value++,
- // this *is* a side effect animationController.forward();
- Instead, it only makes sense to call
forward()
ininitState()
or inside a callback in response to some user interaction. - // intent: create a user document in Firestore // when the user first signs in // this *is** a side effect database.setUserData(UserData( uid: user.uid, email: user.email, displayName: user.displayName ));
- But the call to
database.setUserData()
inside theStreamBuilder
is a side effect. - // this is ok doSomeAsyncWork();
- // this is ok onPressed: doSomeAsyncWork(),
- // this is ok onPressed: () async { await doSomeAsyncWork(); await doSomeOtherAsyncWork(); }
- We can also run asynchronous code inside any event listeners, such as
BlocListener
from flutter_bloc orref.listen()
from Riverpod.
-
-
-
This package is a recommended solution for managing state when using Provider or Riverpod.
Long story short, instead of extending ChangeNotifier, extend StateNotifier:
-
- compare previous and new state
- implement undo-redo mechanism
- debug the application state
-
-
Flutter State Management: Going from setState to Freezed & StateNotifier with Provider
- await widget.dataStore.createProfile(Profile(name: name, id: id));
- setState(() { _isLoading = false; _errorText = null; });
- ChangeNotifier
- bool isLoading = false; String errorText;
- notifyListeners();
- notifyListeners();
- notifyListeners();
- notifyListeners();
- notifyListeners();
- return true;
- final success = await model.submit(name);
- Navigator.of(context).pop();
- final model = context.watch<CreateProfileModel>();
- model
- submit(model, _controller.value.text)
- model.errorText
- submit(model, name)
- model.isLoading
- sealed unions.
- Freezed is a code generation package that offers many useful features. From sealed unions, to pattern matching, to json serialization,
- freezed_annotation:
- build_runner:
- freezed:
- with ChangeNotifier
- notifyListeners();
- notifyListeners();
- notifyListeners();
- notifyListeners();
- notifyListeners();
- Alternatively, we could choose a 3rd party alternative such as
StateNotifier
orCubit
from theflutter_bloc
package. - StateNotifier is a replacement for
ValueNotifier
. You can read about the advantages ofStateNotifier
overValueNotifier
in the package documentation. - StateNotifier<CreateProfileState>
- const CreateProfileState.noError()
- state = CreateProfileState.error('Name can\'t be empty');
- state = CreateProfileState.error('Name already taken');
- state = CreateProfileState.loading();
- state = CreateProfileState.noError();
- state = CreateProfileState.error(e.toString());
- StateNotifierProvider<CreateProfileModel, CreateProfileState>( create: (_) => CreateProfileModel(dataStore), child: CreateProfilePage(), );
- final state = context.watch<CreateProfileState>();
- state.maybeWhen
- state.maybeWhen
- Use StateNotifier
- Use sealed unions
- The Freezed package supports sealed unions via code generation
-
-
AsyncValueWidget: a reusable Flutter widget to work with AsyncValue (using Riverpod)
- AsyncValueSliverWidget<List<Product>>
-
-
Flutter Riverpod Tip: Use AsyncValue rather than FutureBuilder or StreamBuilder
- future: someFuture,
- snapshot
- stream: someStream,
- snapshot
- if (snapshot.connectionState == ConnectionState.waiting)
- } else if (snapshot.hasData) {
- } else if (snapshot.hasError) {
- } else {
- ConsumerWidget
- WidgetRef ref
- final itemValue = ref.watch(itemStreamProvider);
- itemValue.when
- data
- loading
- error
- const ProviderScope(child: MyApp())
- This way, you can call
ref.watch(provider)
, and the widget will rebuild whenever a newAsyncValue
is emitted.
-
-
How to write Flutter apps faster with Riverpod Lint & Riverpod Snippets
- data binding
- reactive caching
- riverpod_annotation:
- build_runner:
- riverpod_generator:
- riverpod_lint:
- analysis_options.yaml
- flutter pub run build_runner watch -d
-
- will this provider return an object, a
Future
, aStream
, or aNotifier
? - should it dispose itself when no longer listened to, or should it keep alive?
- will this provider return an object, a
-
- adding
part
directives using the Riverpod Snippets extension - choosing the right provider (again with Riverpod Snippets)
- filling in the blanks (return type, function name and arguments)
- choosing the correct
Ref
type (Riverpod Lint makes this easier)
- adding
-
-
compute constant - foundation library - Dart API
- compute(_calculate, value);
-
-
How to handle loading and error states with StateNotifier & AsyncValue in Flutter
- ConsumerWidget
- // error handling ref.listen<AsyncValue<void>>( paymentButtonControllerProvider, (_, state) => state.showSnackBarOnError(context), );
- // show a spinner if loading is true isLoading: paymentState.isLoading,
- paymentState.isLoading
- ref.read(paymentButtonControllerProvider.notifier).pay()
- ConsumerWidget
- state.whenOrNull
- paymentState is AsyncLoading<void>;
- bool get isLoading => this is AsyncLoading<void>;
- state.showSnackBarOnError(context),
- final paymentState = ref.watch(paymentButtonControllerProvider);
- paymentState.isLoading,
- paymentState.isLoading
-
-
Flutter App Architecture: The Presentation Layer
- controllers to:
-
- hold business logic
- manage the widget state
- interact with repositories in the data layer
- mediate between our
SignInScreen
and theAuthRepository
- manage the widget state
- provide a way for the widget to observe state changes and rebuild itself as a result
- the type of our
StateNotifier
subclass (SignInScreenController
) - the type of our state class (
AsyncValue<void>
) - We also use the
autoDispose
modifier to ensure the provider's state is disposed when no longer needed. - ConsumerWidget
- final AsyncValue<void> state = ref.watch(signInScreenControllerProvider);
- state
- state
- ref .read(signInScreenControllerProvider.notifier) .signInAnonymously()
- extension AsyncValueUI on AsyncValue
- state.showSnackbarOnError(context),
-
- watching state changes and rebuilding as a result (with
ref.watch
) - responding to user input by calling methods in the controller (with
ref.read
) - listening to state changes and showing errors if something goes wrong (with
ref.listen
)
- watching state changes and rebuilding as a result (with
- And since the controller doesn't depend on any UI code, it can be easily unit tested.
- This makes it an ideal place to store any widget-specific business logic.
- multiple widgets share the same logic
- we need to talk to more than one repository in the same method
-
-
Flutter App Architecture: The Application Layer
- depends on multiple data sources or repositories
- needs to be used (shared) by more than one widget
- In fact, separation of concerns is the #1 reason why we need a good app architecture.
- managing and updating the widget state (that's the job of the controller)
- data parsing and serialization (that's the job of the repositories)
- If we use Riverpod, we also need to define a provider for each of these repositories:
- Passing Ref as an argument
- The logic to mutate the
Cart
should live in the domain layer since it doesn't depend on any services or repositories. - cartService: ref.watch(cartServiceProvider),
-
-
Flutter App Architecture: The Domain Model
- The domain model is a conceptual model of the domain that incorporates both behavior and data.
- Note how the
Product
model is a simple data classes that doesn't have access to repositories, services, or other objects that belong outside the domain layer. - If our business logic is incorrect, we are guaranteed to have bugs in our app. So we have every incentive to make it easy to test, by ensuring our model classes don't have any dependencies.
- explore the domain model and figure out what concepts and behaviors you need to represent
- express those concepts as entities along with their relationships
- implement the corresponding Dart model classes
- translate the behaviors into working code (business logic) that operates on those model classes
- add unit tests to verify the behaviors are implemented correctly
-
-
Flutter Project Structure: Feature-first or Layer-first?
- In other words, a feature is a functional requirement that helps the user complete a given task.
-
-
Flutter App Architecture with Riverpod: An Introduction
- in the Riverpod architecture it makes sense to omit the application layer if it's not needed.
-
-
Flutter App Architecture: The Repository Pattern
- a backend API
- domain layer
- talking to REST APIs
- talking to local or remote databases (e.g. Sembast, Hive, Firestore, etc.)
- talking to device-specific APIs (e.g. permissions, camera, location, etc.)
-
-
Use AsyncValue.guard rather than try/catch inside your StateNotifier subclasses
- try
- catch
- state = await AsyncValue.guard(() => authRepository.signOut());
- guard
-
-
How to Unit Test AsyncNotifier Subclasses with Riverpod 2.0 in Flutter
- The most important part of the test is the call to
container.listen()
. - If you're testing a class that uses
ref
internally, you can't instantiate it directly, as this will lead to aLateInitializationError
. Instead, read the corresponding provider from theProviderContainer
to ensure the provider's state is initialized correctly. - final exception = Exception('Connection failed'); when(authRepository.signInAnonymously).thenThrow(exception);
- any(that: isA<AsyncError>())
- expectLater
- await controller.signInAnonymously();
- the recommended way of writing tests for our notifier classes is to use listeners:
-
- always use type annotations (on your code and tests)
- use matchers such as
any
when you can't create expected values that are 100% equal to the actual values
-
-
How to use Notifier and AsyncNotifier with the new Flutter Riverpod Generator
- These classes are meant to replace
StateNotifier
and bring some new benefits: - Controller
- In the context of the Provider package, which is a popular state management solution for Flutter, a "controller" may not be the most accurate term. However, you could be referring to a "ChangeNotifier" or a "StateNotifier", which serve as controllers in the sense that they manage the state of your application and notify listeners when changes occur.
- ConsumerWidget
- WidgetRef ref
- final counter = ref.watch(counterProvider);
- ref.read(counterProvider.notifier).state++,
- Notifier<int>
- NotifierProvider<Counter, int>
- return Counter();
- Counter.new
- ConsumerWidget
- WidgetRef ref
- final counter = ref.watch(counterProvider);
- ref.read(counterProvider.notifier).state++,
- ref.read(counterProvider.notifier).increment(),
- Notifier with Riverpod Generator
- part 'counter.g.dart'
- @riverpod
- _$Counter
- flutter pub run build_runner watch
- final counterProvider = AutoDisposeNotifierProvider<Counter, int>
- typedef CounterRef = AutoDisposeNotifierProviderRef<int>
- abstract class _$Counter extends AutoDisposeNotifier<int>
- State
- State
- State
- a generic
State
type - import 'counter.g.dart';
- ConsumerWidget
- WidgetRef ref
- final counter = ref.watch(counterProvider);
- ref.read(counterProvider.notifier).state++,
- This is more verbose but also more flexible, as we can add methods with complex logic to our
Notifier
subclasses (much like what we do withStateNotifier
). - part 'counter.g.dart';
- @riverpod
- _$Counter
- @override int build() { return 0; }
- how to store some synchronous state.
- asynchronous state classes using
AsyncNotifier
-
StateNotifier
subclasses to store some immutable state - StateNotifier<AsyncValue<void>>
- this.ref
- const AsyncData(null)
- final Ref ref;
- Future<void>
- async
- final authRepository = ref.read(authRepositoryProvider);
- await
- StateNotifierProvider
- return AuthController(ref);
-
As it turns out, we can't use the
@riverpod
syntax withStateNotifier
.And we should use the new
AsyncNotifier
class instead. - a Notifier implementation that is asynchronously initialized.
- AsyncNotifier<void>
- @override FutureOr<void> build() { // 4. return a value (or do nothing if the return type is void) }
- final authRepository = ref.read(authRepositoryProvider);
- In the
signInAnonymously
method, we read another provider with theref
object, even though we haven't explicitly declaredref
as a property (more on this below). - AsyncNotifierProvider<AuthController, void>
- return AuthController();
- AuthController.new
- Note how the function that creates the provider doesn't have a
ref
argument. - AutoDisposeAsyncNotifier<int>
- AsyncNotifierProvider.autoDispose<AuthController, void>
- AsyncNotifier with Riverpod Generator
- part 'auth_controller.g.dart';
- @riverpod
- _$AuthController
- @override FutureOr<void> build() { // 6. return a value (or do nothing if the return type is void) }
- final authRepository = ref.read(authRepositoryProvider);
- await
- AutoDisposeAsyncNotifierProvider<AuthController, void>
- typedef AuthControllerRef = AutoDisposeAsyncNotifierProviderRef<void>;
- abstract class _$AuthController extends AutoDisposeAsyncNotifier<void>
- State
- State
- State
- And this means we can set the state to
AsyncData
,AsyncLoading
, orAsyncError
in thesignInAnonymously
method. - Example with Asynchronous Initialization
- _$SomeOtherController
- Future<String>
- async
- In this case, the
build
method is truly asynchronous and will only return when the future completes. - ConsumerWidget
- WidgetRef ref
- valueAsync.when(...);
-
- once with a temporary
AsyncLoading
value on first load - again with the new
AsyncData
value (or anAsyncError
) when the initialization is complete
To handle this, the controller will emit two states, and the widget will rebuild twice:
- once with a temporary
- int someValue
- someOtherControllerProvider(42)
- New in Riverpod 2.3: StreamNotifier
- Stream<int>
- _$Values
- Stream<int>
-
Figuring out the correct syntax for all combinations of providers and modifiers (
autoDispose
andfamily
) was one major pain point with Riverpod.But with the new riverpod_generator package, all these problems go away as you can leverage
build_runner
and generate all the providers on the fly.
-
-
How to Auto-Generate your Providers with Flutter Riverpod Generator
- access dependencies in our code
- cache asynchronous data from the network
- manage local application state
- riverpod:
- riverpod_annotation:
- build_runner:
- riverpod_generator:
- flutter pub run build_runner watch -d
- // 1. import the riverpod_annotation package
- // 2. add a part file
- // 3. use the @riverpod annotation
- // 4. update the declaration
- This means that we only need to write this code:
- When creating a provider for a
Repository
, don't add the@riverpod
annotation to theRepository
class itself. Instead, create a separate global function that returns an instance of thatRepository
and annotate that. We will learn more about using@riverpod
with classes in the next article. - Future<TMDBMovie>
- ConsumerWidget
- final movieAsync = ref.watch(movieProvider(movieId: movieId));
- error
- loading
- data
- { required int movieId, }
- a realtime database such as Cloud Firestore
- web sockets
- @Riverpod(keepAlive: false)
- to implement some custom caching behaviour
- // get the [KeepAliveLink] final link = ref.keepAlive();
- // dispose on timeout link.close();
- timer.cancel()
- And since CI build minutes are not free, I recommend adding all the generated files to version control (along with
.lock
files to ensure everyone runs with the same package versions). - Legacy providers like
StateProvider
,StateNotifierProvider
, andChangeNotifierProvider
are not supported, and I've already explained how they can be replaced in my article about how to useNotifier
andAsyncNotifier
with the new Flutter Riverpod Generator.
-
댓글