e.g. I have class ProfileModel with bunch of fields
many of them don't have default values unless they're initialising when I get user info from backend
with riverpod I need to write something like
final profileProvider = StateNotifierProvider((ref) => ProfileState());
class ProfileState extends StateNotifier<ProfileModel> {
ProfileState() : super(null);
}
I understand I need to pass something like ProfileState.empty() into super() method instead passing null
but in this case I have to invent default values for every ProfileModels fields
this sounds weird for me, I don't want to break my head to care about empty or default state of EVERY model in project
in my example there are no default values for user name, age etc
this is pure immutable class
what I'm doing wrong or missing?
or I can declare model as nullable extends StateNotifier<ProfileModel?>
but I'm not sure is this a good way
It is fine to use the StateNotifier with a nullable model. If you semantically want to indicate the value can be actually absent, I would say that that having null is alright.
However, what I usually do and what I think is better, is create a state model that contains the model, but also properties that relate to the different states the app could be in.
For example, while fetching the data for the model from an API, you might want to have a loading state to show a spinner in the UI while waiting for the data to be fetched. I wrote an article about the architecture that I apply using Riverpod.
A simple example of the state model would be:
class ProfileState {
final ProfileModel? profileData;
final bool isLoading;
ProfileState({
this.profileData,
this.isLoading = false,
});
factory ProfileState.loading() => ProfileState(isLoading: true);
ProfileState copyWith({
ProfileModel? profileData,
bool? isLoading,
}) {
return ProfileState(
profileData: profileData ?? this.profileData,
isLoading: isLoading ?? this.isLoading,
);
}
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ProfileState &&
other.profileData == profileData &&
other.isLoading == isLoading;
}
#override
int get hashCode => profileData.hashCode ^ isLoading.hashCode;
}
Related
Let's say I have few properties which describe a single context device (keepEmail is bool, email is String, deviceToken is String, themeMode is ThemeMode enumeration.
As they are belong to single domain object I think I need to create a DeviceState class (because I would like to save this object in web local storage as {"keepEmail": true, "email": "email#email.com", ...}
class DeviceState {
final bool keepEmail;
final String email;
final String deviceToken;
final ThemeMode themeMode;
// other constructors and methods
}
which in its turn is a part of ApplicationState:
class ApplicationState extends ChangeNotifier {
DeviceState get deviceState => localStorage['deviceState'];
set deviceState(DeviceState state) {
if(localStorage['deviceState'] != state) {
localStorage['deviceState'] = state;
notifyListeners();
}
}
// other states can be here
}
But here the chip is coming. Some of properties must be observables to refresh the navigator (I use package GoRouter which can listen Listenable to refresh its state) or user interface.
Namely change of deviceToken must launch router redirect (guard) system which checks if device token is set do something, and change of themeMode must refresh current theme.
So the question is if it is wise to combine all application states (which certainly can be persisted) like themes, languages, scroll positions, selected tabs etc to a single ApplicationState which is ChangeNotifier or create ChangeNotifier for all pieces of changed data and then aggregate them into single class:
class DeviceState extends ChangeNotifier {
String get deviceToken;
}
class LoginState extends ChangeNotifier {
String get accessToken;
}
class GoogleMapState extends ChangeNotifier {
MapStyle get mapStyle;
}
//... and many many other similar classes
class ApplicationState {
DeviceState deviceState;
AccessState accessState;
GoogleMapState mapState;
//...
}
and then use required Listenable in a specific place?
So I am a little lost how to implement corectly and I am ready to hear some ideas and advices.
I am making first steps with Riverpod and just want to check if my understanding of handling changes of some data class properties using Riverpod is correct.
Imagine, I have a data class like that:
class MyClass {
final String name;
final int id;
const MyClass(this.name, this.id);
}
Then I create a StateNotifier:
class MyClassStateNotifier extends StateNotifier<MyClass> {
MyClassStateNotifier(MyClass state) : super(state);
void setName(String name) {
state.name = name;
}
}
And this won't work - UI will not be rebuilt after calling setName this way.
So I need to modify classes in the following way:
class MyClass {
final String name;
final int id;
const MyClass(this.name, this.id);
MyClass copyWith({name, id}) {
return MyClass(name ?? this.name, id ?? this.id);
}
}
and the StateNotifier as following:
class MyClassStateNotifier extends StateNotifier<MyClass> {
MyClassStateNotifier(MyClass state) : super(state);
void setName(String name) {
state = state.copyWith(name: name);
}
}
This pair will work and the UI will be rebuilt.
So, my question: does one always need to reinstantiate the object in this way?..
From my perspective, this is a bit strange (simple datatypes like String / int do not require this) and the boilerplate for copyWith method might become pretty huge if I have a dozen of object's properties.
Is there any better solution available for Riverpod or is it the only one and correct?..
Thanks in advance! :)
To trigger a state change you have to use the state setter. The implementation looks like this:
#protected
set state(T value) {
assert(_debugIsMounted(), '');
final previousState = _state;
_state = value;
/// only notify listeners when should
if (!updateShouldNotify(previousState, value)) {
return;
}
_controller?.add(value);
// ...
The internal StreamController<T> _controller needs to be triggered (add), to notify listeners (in this case riverpod) about updates.
By using state.name = something you're not informing the StateNotifier about a new state (not calling the state setter). Only your object holds the new value but nobody was notified.
Your state is mutable and that very often leads to such misbehavior. By using an immutable object you can prevent such errors in the first place. Write it yourself or use freezed.
Learn more about immutability in my talk
Im using bloc and it was working as expected but today i notice a strage behaviour when i was sending the same state (RefreshState) using copyWith, the state wasnt trigger after second call. then i did a test creating two objects and compared them but the result was they are the same object, very odd.
So why is this happen?, this is my class:
class Model extends Equatable {
final List<Product> mostBuyProducts;
const Model({
this.mostBuyProducts,
});
Model copyWith({
List<Product> mostBuyProducts,
}) =>
Model(
mostBuyProducts: mostBuyProducts ?? this.mostBuyProducts,
);
#override
List<Object> get props => [
mostBuyProducts,
];
}
and then i use the CopyWith method like (inside the bloc):
Stream<State> _onDeleteProduct(OnDeleteProduct event) async* {
state.model.mostBuyProducts.removeWhere((p) => p.id == event.id);
var newMostBuyProducts = List<Product>.from(state.model.mostBuyProducts);
final model1 = state.model;
final model2 = state.model.copyWith(mostBuyProducts: newMostBuyProducts);
final isEqual = (model1 == model2);
yield RefreshState(
state.model.copyWith(mostBuyProducts: newMostBuyProducts));
}
isEqual return true :/
BTW this is my state class
#immutable
abstract class State extends Equatable {
final Model model;
State(this.model);
#override
List<Object> get props => [model];
}
Yes because lists are mutable. In order to detect a change in the list you need to make a deep copy of the list. Some methods to make a deep copy are available here : https://www.kindacode.com/article/how-to-clone-a-list-or-map-in-dart-and-flutter/
Using one such method in the solution below! Just change the copyWith method with the one below.
Model copyWith({
List<Product> mostBuyProducts,
}) =>
Model(
mostBuyProducts: mostBuyProducts ?? [...this.mostBuyProducts],
);
I'm learning and building a Flutter app using BLoC pattern and in a lot of tutorials and repositories, I have seen people having a separate class for each state of the BLoC and others which have a single state class with all the properties defined.
Is there a standard BLoC way for defining state classes or is it a personal choice?
Example with multiple state classes
abstract class LoginState extends Equatable {
LoginState([List props = const []]) : super(props);
}
class LoginInitial extends LoginState {
#override
String toString() => 'LoginInitial';
}
class LoginLoading extends LoginState {
#override
String toString() => 'LoginLoading';
}
class LoginFailure extends LoginState {
final String error;
LoginFailure({#required this.error}) : super([error]);
#override
String toString() => 'LoginFailure { error: $error }';
}
Example with a single state class
#immutable
class MyFormState extends Equatable {
final String email;
final bool isEmailValid;
final String password;
final bool isPasswordValid;
final bool formSubmittedSuccessfully;
bool get isFormValid => isEmailValid && isPasswordValid;
MyFormState({
#required this.email,
#required this.isEmailValid,
#required this.password,
#required this.isPasswordValid,
#required this.formSubmittedSuccessfully,
}) : super([
email,
isEmailValid,
password,
isPasswordValid,
formSubmittedSuccessfully,
]);
factory MyFormState.initial() {
return MyFormState(
email: '',
isEmailValid: false,
password: '',
isPasswordValid: false,
formSubmittedSuccessfully: false,
);
}
MyFormState copyWith({
String email,
bool isEmailValid,
String password,
bool isPasswordValid,
bool formSubmittedSuccessfully,
}) {
return MyFormState(
email: email ?? this.email,
isEmailValid: isEmailValid ?? this.isEmailValid,
password: password ?? this.password,
isPasswordValid: isPasswordValid ?? this.isPasswordValid,
formSubmittedSuccessfully:
formSubmittedSuccessfully ?? this.formSubmittedSuccessfully,
);
}
#override
String toString() {
return '''MyFormState {
email: $email,
isEmailValid: $isEmailValid,
password: $password,
isPasswordValid: $isPasswordValid,
formSubmittedSuccessfully: $formSubmittedSuccessfully
}''';
}
}
Which one should be used when?
What's the advantage and disadvantage between both?
It's more of a personal choice / coding style but I agree that some way might be better than other depending on the scenario.
Note that Bloc doc now states the two methods.
There a few differences though. First, when using multiple state classes you have greater control about the fields of each state. For example if you know that an error can only happen in some case it might be better to isolate this in a specific class BlocSubjectError instead of having a class containing a nullable error that might or might not be there.
From a caller perspective the conditions are also a bit different, you might prefer one way or the other :
With multiple state classes
if(state is BlocSubjectLoading) {
// display loading indicator
}
else if (state is BlocSubjectError) {
// display errror
} else if (state is BlocSubjectSuccess) {
// display data
}
With single state class
if(state.isLoading) {
// display loading indicator
}
else if (state.error != null) {
// display errror
} else if (state.data != null) {
// display data
}
Though it might not look like a big difference, the second example allows for mixed states : you could have both an error and a data, or both being loading and having a previous data. It might or might be what you desire. In the first case the types make this more constrained which make this technique closer to representing the state of your app with types, a.k.a. Type Driven Development.
On the other hand, having multiple state classes might be cumbersome if they all declare the same fields because you will need to instantiate your states with all the parameters each time whereas using a single class you can just call a .copyWith() method to which you will only give changed parameters.
The single state class pattern is close to Triple Pattern (or Segmented State Pattern). You can find a comprehensive workshop that describes this pattern with Bloc here.
In the login view, if the user taps on the login button without having inserted his credentials, the LoginFailState is yield and the view reacts to it. If he taps again, this LoginFailstate is yield again, but the view doesn't react to it. So, is there a way to yield more times the same state?
There is some code to better explain my situation:
class LoginBloc extends Bloc<LoginEvent, LoginState> {
#override
LoginState get initialState => LoginUninitialized();
#override
Stream<LoginState> mapEventToState(LoginEvent event) {
if (event is loginButtonPressed) {
yield LoginFailState();
}
}
View:
#override
Widget build(BuildContext context) {
return BlocBuilder(
bloc: _loginBloc,
builder: (BuildContext context, LoginState state) {
if (state is LoginFail) {
print ('Login fail');
}
return Column(
...
)
You can receive an update for the "same" State if you don't extend Equitable, or implement your own '==' logic which makes the two LoginFailStates equal.
The solution is to yield a different State in between, like in the Bloc example.
yield LoginLoading();
It gets called on every login button tap. Felangel's LoginBloc example.
By default BLoC pattern will not emit state when the same state will be passed one after another. One way to do this is to pass your initial BLoC state after passing LoginFailState.
So after user clicks on the button with wrong credentials passed states will not be:
LoginFailState()
LoginFailState()
but
LoginFailState()
LoginEmptyState()
LoginFailState()
LoginEmptyState()
Which will make UI react to each of them.
But I think that the best and cleanest solution is to pass LoadingState from BLoC before passing LoginFailState().
You can follow the blog post that I have recently written regarding this topic.
Problem
When you try to emit a new state that compares equal to the current state of a Bloc, the new state won't be emitted.
This behavior is by design and is discussed here.
When I say "compares equal" I mean that the == operator for the two state objects returns true.
Solution
There are two proper approaches:
Your state class should NOT extend Equatable. Without Equatable, two objects of the same class with the same fields will NOT compare as equal, and this new state will always be emitted.
Sometimes you need your state class to extend Equatable. In this case, just add the result of the identityHashCode(this) function call to your props getter implementation:
class NeverEqualState extends Equatable {
#override
List<Object?> get props => [identityHashCode(this)];
}
Note that I use identityHashCode that works regardless the operator == is overloaded or not. In contrast, hashCode will not work here.
Warning:
Do not use random values in the getter implementation List<Object> get props => [Random().nextDouble()];. Random variables are random, meaning that with extremely low probability you still might get two equal values in a sequence that will break this workaround. This is extremely unlikely, so it's not possible to reproduce and debug this.
You can and should include fields in your get props implementation, but keep in mind that when all fields compare as equal the objects will also compare as equal.
Emitting some other state in-between two equal states works but it forces your BlocBuilder to rebuild part of UI and BlocListener to execute some logic. It's just inefficient.
Finally, why would you like to have a state class extend Equatable but still not compare equal? This might be needed when your state class is actually the root of a hierarchy, where some descendants need to implement the == operator properly, and some need to never compare equal. Here is the example:
class BaseMapState extends Equatable {
const BaseMapState();
#override
List<Object?> get props => [];
}
class MapState extends BaseMapState {
final Map<String, Report> reports;
final Report? selectedReport;
final LatLng? selectedPosition;
final bool isLoadingNewReports;
const MapState(
{this.reports = const {},
this.selectedReport,
this.selectedPosition,
this.isLoadingNewReports = false});
#override
List<Object?> get props => [
...reports.values,
selectedReport,
selectedPosition,
isLoadingNewReports
];
}
class ErrorMapState extends BaseMapState {
final String? error;
const ErrorMapState(this.error);
#override
List<Object?> get props => [identityHashCode(this), error];
}
class NeedsAuthMapState extends ErrorMapState {
const NeedsAuthMapState() : super('Authentication required');
}
class NoInternetMapState extends ErrorMapState {
const NoInternetMapState() : super("No Internet connection");
}
If you use Equitable and tries to emit two equal instances of the same State with different properties, make sure that you override props array. By overriding props array, Equitable will know how to compare state instances.
class TablesLoadedState extends Equatable {
final List<TableEntity> tablesList;
TablesLoadedState(this.tablesList);
#override
List<Object> get props => [tablesList];
}
So, when bloc emits two instances of the same state with different values, these state-instances will be passed to BlocListener and UI will be updated according to new data.
A late possible workaround would be adding a random double to the state get props, this way the state won't be equal and you can yield them one after the other if you want.
also, Random().nextDouble() complexity is O(1) so you don't need to worry about performance
class LoginFailState extends LoginState {
#override
List<Object> get props => [Random().nextDouble()];
}