Hooks are a way to reuse code more efficiently and are not technically a state-management solution ( docs )
- hooks can only be used in the
build
method of a widget - hooks are retrieved by their index, and any change in their order will affect all of the hooks that come after
- hooks aren't rebuilt with hot-reloads as the
HookWidget
holds on to them, and if it finds they haven't been changed ( depending on the index and class ) they won't be rebuilt useTextingEditingController
handles the initialization and disposing of the controller as any hook you create within aHookWidget
is registered within that instance using it's index and class so the lifetime of the controller would be automatically handledHookWidget
is aStatelessWidget
so in order to create and maintain a state we use theuseState
hook which returns aValueNotifier
useEffect
Useful for side-effects and optionally canceling them.
useEffect is called synchronously on every
build
, unlesskeys
is specified. In which case useEffect is called again only if any value insidekeys
as changed.It takes an
effect
callback and calls it synchronously. Thateffect
may optionally return a function, which will be called when theeffect
is called again or if the widget is disposed.By default
effect
is called on everybuild
call, unlesskeys
is specified. In which case,effect
is called once on the first useEffect call and whenever something withinkeys
change
-
useFuture
can be used to create anAsyncSnapshot
that rebuilds a widget whenever the future is resolved -
useMemoized
allows for caching of complex objects. If it has a cached version of that object from before it will return it otherwise it will kick off that future and hold on to it afterwards ( I wanna create something, hold on to it ) -
useAppLifecycleState
can be used to keep track of the state of the application e.g. resumed, paused, inactive, or detached -
useEffect
can be the equivalent ofinitState
+didUpdateWidget
+dispose
( depending on the implementation ) -
hooks should NOT be used within loops, conditionally, or inside a user interaction lifecycle such as
onPressed
( read more )
-
I learned that this guy "Remi Rousselet" is fkin insane
-
three packages,
-
riverpod
, Dart only (NO flutter) -
flutter_riverpod
, A basic way of using Riverpod with flutter. You mostly use it for state management. -
If you are using hook widgets(
flutter_hooks
) that reduce the boilerplate code like dispose, you can usehooks_riverpod
-
-
ref
is basically your window to the outside world. Providers are usually global functions with no idea about any other variable unless they're global and that's whereref
comes in. -
ProviderScope
should be used to specify the scope of the providers whether for example for the entire app ( at the root of the application ), or actually for the widgets or part of the tree that needs those providers. -
"it is possible to insert other
ProviderScope
anywhere inside the widget tree to override the behavior of a provider for only a part of the application" ProviderScope class - flutter_riverpod library - Dart API -
ref.watch
can be used to "watch" or listen to changes within a provider -
ref.read
obtains the current snapshot of the object specified. ( it reads basically, duh ) -
Consumer
can be used to be more efficient in rebuilding the UI, you only rebuild the parts of the UI the actually needs to be rebuilt.Consumer class - flutter_riverpod library - Dart API
"If you pass the pre-built subtree as the child parameter, the Consumer will pass it back to your builder function so that you can incorporate it into your build. Using this pre-built child is entirely optional, but can improve performance significantly in some cases and is therefore a good practice."
[StateProvider] vs [StateNotifierProvider] SP: provides you with a getter/setter [state] that can be changed from the outside SNP: provides you with a getter [state] that can be manipulated with prediffined methods " StateProvider is a provider that exposes a way to modify its state. It is a simplification of StateNotifierProvider, designed to avoid having to write a StateNotifier class for very simple use-cases. StateProvider exists primarily to allow the modification of simple variables by the User Interface. The state of a StateProvider is typically one of: an enum, such as a filter type a String, typically the raw content of a text field a boolean, for checkboxes a number, for pagination or age form fields You should not use StateProvider if: your state needs validation logic your state is a complex object (such as a custom class, a list/map, ...) the logic for modifying your state is more advanced than a simple count++. For more advanced cases, consider using StateNotifierProvider instead and create a StateNotifier class. While the initial boilerplate will be a bit larger, having a custom StateNotifier class is critical for the long-term maintainability of your project – as it centralizes the business logic of your state in a single place. " Sources ( official docs are really good ) - https://developermemos.com/posts/different-providers-riverpod - https://riverpod.dev/docs/providers/state_provider/ - https://riverpod.dev/docs/providers/state_notifier_provider/
-
FutureProvider
allows you to handle various states ( loading, done, error ) veryconveniently with the
when
method -
FutureProvider
is used for caching asynchronous computations, and if thisFuture
is used in multiples places throughout the code, it'll only be resolved once unless you're e.g. combining other providers states -
all different types of providers at their core conform to another provider
AlwaysAliveProviderBase
so you can work with different types of providers easily or somewhat generically -
under the hood, riverpod checks for equality when providing us with new values so we might wanna override the equality to work as expected
-
.family
modifier can be used to get a unique provider based on external parameters. docs -
.autoDispose
modifier can be used to dispose of a provider when it's no longer being used ( using auto-dispose adds an extra property to the ref objectkeepAlive
if you want to programatically decide whether or not you should dispose off the provider ) -
ref.read
should be avoided as much as possible even when the state may never change. Even then,ref.watch
should be used for maintability purposes. ref.read - docs -
ref.refresh
can be used to reset the value of the provider -
.select
can be used to explicitly tell Riverpod that we want to listen to a certain property that we care aboutUsing StreamProvider over StreamBuilder has numerous benefits: * it allows other providers to listen to the stream using ref.watch. * it ensures that loading and error cases are properly handled, thanks to AsyncValue. * it removes the need for having to differentiate broadcast streams vs normal streams. * it caches the latest value emitted by the stream, ensuring that if a listener is added after an event is emitted, the listener will still have immediate access to the most up-to-date event. * it allows easily mocking the stream during tests by overriding the StreamProvider.
-
ProviderObserver
can be used to observe changes in aProviderContainer
such as adding, disposing, or updating a provider
-
Equality in dartlang is referential by default meaning it compares references to objects.
When instantiating an object
final potato = Potato();
the variablepotato
actually referes or points to an object the lives in the memory. Unless a constant constructer is used ( both in the defintion and declaration ) and instantiated with the same values, each object would refer to or instantiate a new object.void main(){ final mrPotato1 = Potato('Michael Scott', '123'); final mrPotato2 = Potato('Michael Scott', '123'); print(mrPotato1 == mrPotato2); // false // mrPotato1 --refersTo--> Object1 in the memory // mrPotato2 --refersTo--> Object2 in the memory final mrPotato3 = const Potato('Michael Scott', '123'); final mrPotato4 = const Potato('Michael Scott', '123'); print(mrPotato3 == mrPotato4); // true // mrPotato3 --refersTo--> Object3 in the memory // mrPotato4 --refersTo--> Object3 in the memory } @immutable class Potato{ final String name; final String ssn; const Potato(this.name, this.ssn); }
class Potato{ ... /// To fix this, we can override the equality operator to be value equlaity instead @override bool operator ==(covariant Potato other) => identical(this, other) || // by defintion, if they reference the same object then they're equal other is Potato && other.runtimeType == runtimeType && other.name == name && other.ssn == ssn; // or [other.hashCode == hashCode] instead of comparing the attributes indivdually @override int get hashCode => name.hashCode ^ ssn.hashCode; }
Dart Data Class Generator vscode extension
equatable | Dart Package ( no need for explicit overriding )
- The
_test
in the filename syntax marks the file as a testing file.
- testing a small piece or a "unit" of code possibly with fake data to mock an api call or the original data source
test("Define what the test will do", (){
// Arrange: Get the resources needed for testing
// Act: Perform operations you need
// Assert: Check if the output generated is the one you expected
});
-
if you wish to perform ur test with the real API, ur gonna need to create ur own
HttpClient
which can be done by extendingHttpOverrides
and then usingHttpOverrides.global = _MyHttpOverrides();
inside the test -
common instructions can be refactored into the
setUpAll
function which is executed before any test ( docs )setUpAll(() async { HttpOverrides.global = _MyHttpOverrides(); });
-
related tests can be grouped in a
group
function to be run together
- checking to see if a widget (UI) behaves as expected with real or mocked data
testWidget('desc',
(tester) async {
/// if you wish to use async code you have to use `runAsync`
/// as widget testing doesn't support real async code
await tester.runAsync(() async {
//
});
});
-
.pumpWidget
"Renders the UI from the givenwidget
" -
.pumpAndSettle
"This essentially waits for all animations to have completed" -
you can interact with the widget through the
tester
as it exposes methods such as.tap
or.enterText
on a given widget that can be found through e.g. typefind.byType
(see more finders) -
.pump
acts like asetState
-
for test assert we can use a
matcher
(all matchers)
-
testing the complete application flow in real-world conditions
-
The integration test runs in a real device
-
we need to add a dev dependancy to our project
integration_test: sdk: flutter
"Integration tests are used to check the complete app or to check the flow of the application. So here real HTTP requests are made and run in a real async environment. So we don’t need to create our custom HttpClient or use
tester.runAsync
to run asynchronous statements."
-
directory structure
lib/ ... integration_test/ foo_test.dart bar_test.dart test/ # Other unit tests go here
-
basic setup would look like
void main() { final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding; // fullyLive ensures that every frame is shown, suitable for heavy animations and navigation if (binding is LiveTestWidgetsFlutterBinding) { binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; } }
" You should only use
testWidgets
to declare your tests, or errors won’t be reported correctly."
unit | widget | integration | |
---|---|---|---|
speed | fastest | fast | slow |
dependencies | few | more | most |
usage freq. | high | low | moderate |