Publish to my blog (weekly)

    • 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.
    • 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>
    • / 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).
    • 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
      • 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 the onPressed callback, and this is only called when we tap on the button - independently from the build() 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() in initState() 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 the StreamBuilder 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 or ref.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
    • 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 or Cubit from the flutter_bloc package.
    • StateNotifier is a replacement for ValueNotifier. You can read about the advantages of StateNotifier over ValueNotifier 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
    • AsyncValueSliverWidget<List<Product>>
    • 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 new AsyncValue is emitted.
    • 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, a Stream, or a Notifier?
      • should it dispose itself when no longer listened to, or should it keep alive?
      • 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)
    • compute(_calculate, value);
    • 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
    • controllers to:
      • hold business logic
      • manage the widget state
      • interact with repositories in the data layer
    • mediate between our SignInScreen and the AuthRepository
    • 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)
    • 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
    • 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),
    • 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
    • In other words, a feature is a functional requirement that helps the user complete a given task.
    • in the Riverpod architecture it makes sense to omit the application layer if it's not needed.
    • 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.)
    • try
    • catch
    • state = await AsyncValue.guard(() => authRepository.signOut());
    • guard
    • 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 a LateInitializationError. Instead, read the corresponding provider from the ProviderContainer 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
    • 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 with StateNotifier).
    • 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 with StateNotifier.

      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 the ref object, even though we haven't explicitly declared ref 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, or AsyncError in the signInAnonymously 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(...);
      • To handle this, the controller will emit two states, and the widget will rebuild twice:

        • once with a temporary AsyncLoading value on first load
        • again with the new AsyncData value (or an AsyncError) when the initialization is complete
    • 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 and family) 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.

    • 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 the Repository class itself. Instead, create a separate global function that returns an instance of that Repository 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, and ChangeNotifierProvider are not supported, and I've already explained how they can be replaced in my article about how to use Notifier and AsyncNotifier with the new Flutter Riverpod Generator.

Posted from Diigo. The rest of my favorite links are here.

댓글

이 블로그의 인기 게시물

Publish to my blog (weekly)

Publish to my blog (weekly)