Flutter Riverpod: Filter rebuilds with StateNotifier and .select() - flutter

This is my current state management solution
class UserState {
final int id;
final String name;
}
class UserNotifier extends StateNotifier<UserState> {
UserNotifier() : super(User(1, 'Pero Peric'));
}
final userNotifierProvider = StateNotifierProvider((ref) => UserNotifier());
I want to rebuild my UI only when the name changes not the id!
Riverpod provides a way to do this link but I can't get it working with my StateNotifier.
I would write it like this but it isn't working like this.
// inside Consumer widget
final userName = watch(userNotifierProvider.select((value) => value.state.name));
Can you refactor my code to work or propose another solution?
Any help is appreciated!

According to the doc, "this method of listening to an object is currently only supported by useProvider from hooks_riverpod and ProviderContainer.listen".
Try to create another provider, which you can use in UI.
final nameProvider = StateProvider<String>((ref) => ref.watch(userNotifierProvider.select((user) => user.name)));

class UserState {
UserState({
required this.id,
required this.name,
});
final int id;
final String name;
}
class UserNotifier extends StateNotifier<UserState> {
UserNotifier() : super(UserState(id: 1, name: 'Pero Peric'));
}
final userNotifierProvider =
StateNotifierProvider<UserNotifier, UserState>((ref) => UserNotifier());
In consumer,
final userName =
watch(userNotifierProvider.select((value) => value.name)); // value is the state

Related

How do I update a Map with BLoC and equatable?

I am still a beginner with BLoC architecture. So far the UI updates when using int, bool, and other basic data types. But when it comes to Maps it really confuses me. My code basically looks like this:
my state
enum TalentStatus { initial, loading, loaded, error }
class TalentState extends Equatable {
const TalentState({
required this.talentStatus,
this.selectedService = const {},
required this.talents,
this.test = 0,
});
final TalentStatus talentStatus;
final Talents talents;
final Map<String, Service> selectedService;
final int test;
TalentState copyWith({
TalentStatus? talentStatus,
Talents? talents,
Map<String, Service>? selectedService,
int? test,
}) =>
TalentState(
selectedService: selectedService ?? this.selectedService,
talentStatus: talentStatus ?? this.talentStatus,
talents: talents ?? this.talents,
test: test ?? this.test,
);
#override
List<Object> get props => [talentStatus, talents, selectedService, test];
}
my event
abstract class TalentEvent extends Equatable {
const TalentEvent();
#override
List<Object> get props => [];
}
class TalentStarted extends TalentEvent {}
class TalentSelectService extends TalentEvent {
const TalentSelectService(
this.service,
this.talentName,
);
final Service service;
final String talentName;
}
and my bloc
class TalentBloc extends Bloc<TalentEvent, TalentState> {
TalentBloc(this._talentRepository)
: super(TalentState(
talentStatus: TalentStatus.initial, talents: Talents())) {
on<TalentSelectService>(_selectService);
}
final TalentRepository _talentRepository;
Future<void> _selectService(
TalentSelectService event,
Emitter<TalentState> emit,
) async {
state.selectedService[event.talentName] = event.service;
final selectedService = Map<String, Service>.of(state.selectedService);
emit(
state.copyWith(
selectedService: selectedService,
),
);
}
}
whenever an event TalentSelectService is called BlocBuilder doesn't trigger, what's wrong with my code?
Your Service object must be comparable. One suggestion is that it extends Equatable. Either way it have to implement (override) the == operator and hashCode
The reason your BlocBuilder doesn't trigger is (probably) that it doesn't recognize that there has been a change in the Map.

Why when i use a class in dart with equatable and just a list as property the copyWith method return the same object, same hascode

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],
);

Provider automatically gets updated when calling setState

I am new to Flutter and currently working with Providers. I am pulling some static array list from an api and saving it to the Provider. I am letting the user to select from this list and attach to the Item he is creating using the form.
So, Everytime the user tries to create a new Item, he/she should see the static list with the selection set to false.
But, the provider array variable gets automatically updated upon calling setState. Below is the Issue I'm facing..
main.dart
MultiProvider(
providers: [
ChangeNotifierProvider<Info1Class>(
create: (ctx) => Info1Class(),
),
ChangeNotifierProvider<Info2Class>(
create: (ctx) => Info1Class(),
),
],
child: MaterialApp(
And in my Stateful widget in the build method. I am getting the Provider Details like this.
screenArray.clear();
final t = Provider.of<Info1Class>(context, listen: false).dataArray;
screenArray.addAll(t);
Whenever I call setState to update the elements of screenArray, the provider data gets updated as well.
setState(() {screenArray[0].selected = true})
After setState(), if I print the Provider dataArray's first element, it showing as true.
print(Provider.of<Info1Class>(context, listen: false).dataArray[0].selected)
My Dependancies
provider: ^4.3.2+4
Is there a way to avoid the Provider data getting updated and only update the variable in my Stateful Widget ?
Please let me know if I am missing something.. Thanks for your help.
I tried Getx and Provider both for this problem, it's problem of referencing of object not the Provider or GetX,
I was coping reference of Objects, list or all data. to Solve this problem, I create clone of each object and then use it.
// COMMON
String? uid;
String? username;
String? fullName;
String? email;
num? timestamp;
List<ProfilePhoto>? photos;
List<String>? skills;
I add one object list and other simple String list
Clone class
MyUser.clone(MyUser? myUser) {
uid = myUser?.uid;
username = myUser?.username;
fullName = myUser?.fullName;
userType = myUser?.userType;
email = myUser?.email;
timestamp = myUser?.timestamp;
photos = ProfilePhoto.cloneList(myUser?.photos);
status = myUser?.status;
skills = [...(myUser?.skills ?? [])];
}
Constructor
MyUser({
this.uid,
this.username,
this.fullName,
this.email,
this.timestamp,
this.photos,
this.skills,)};
Photo class
class ProfilePhoto {
String? id;
String? title;
String? url;
File? file;
bool? isDefault;
ProfilePhoto({
this.id,
this.title,
this.url,
this.file,
this.isDefault,
});
ProfilePhoto.clone(ProfilePhoto profilePhoto) {
id = profilePhoto.id;
title = profilePhoto.title;
url = profilePhoto.url;
file = profilePhoto.file;
isDefault = profilePhoto.isDefault;
}
static List<ProfilePhoto>? cloneList(List<ProfilePhoto>? items) {
if (items == null) return [];
List<ProfilePhoto>? newItems = [];
for (ProfilePhoto item in items) {
newItems.add(ProfilePhoto.clone(item));
}
return newItems;
}
}
Screen layout
#override
void didChangeDependencies() {
super.didChangeDependencies();
final data = _userController.user;
MyUser? user = MyUser.clone(data);
}
void _onChangeDefault(int index) {
ProfilePhoto pp = _profilePhotos[index];
setState(() {
_profilePhotos.removeAt(index);
_profilePhotos.insert(0, pp);
});
}
this may be not a good or optimized solution, but this solve my problem of auto update data in state manager

Using a provider in a custom class flutter

I am creating a program that needs to use Provider to get values. I call my provider like below in a stateful widget
final user = Provider.of<Users>(context);
Now I would like to use the provider in a custom class
class UserInformation {
final user = Provider.of<Users>(context):
}
This won't work because context is not defined. kindly assist on how I can do this without using a BuildContext.
This is my class Users that I have on a separate dart file and use as a model for for my data streams.
class Users {
final String uid;
final String name;
final String department;
final String position;
Users({ this.uid, this.department, this.name, this.position });
}
This is the query I use to pull data from firestore
Stream<List<FormQuestions>> get userData{
return userCollection.where('name', isEqualTo: 'First Name').where('department', isEqualTo: 'department').snapshots()
.map(_userDataFromSnapshot);
}
I would like the name to be a variable that I get from say (user.name) from the model class. and the same for the department.
Thanks.
You can only access classes which are ChangeNotifiers in the descendant widgets in the tree below this provided ChangeNotifier because behind the scenes the Provider uses InheritedWidget (which uses context) to provide you with the ChangeNotifier you put up in the tree
So in your case there is no way to access the Users from UserInformation and you have to alter your code to make a workaround
Edit: this is a suggestion to achieve what you want if you are using this code inside a widget:
class UserInformation{
final User user;
UserInformation(this.user);
}
class SomeWidget extends StatefulWidget {
#override
_SomeWidgetState createState() => _SomeWidgetState();
}
class _SomeWidgetState extends State<SomeWidget> {
void someMethod(){
final User user = Provider.of<Users>(context);
UserInformation userInformation = UserInformation(user);
//do something
}
#override
Widget build(BuildContext context) {
return Container();
}
}
¡Hey! In the class you need to add "with ChangeNotifier":
class Users with ChangeNotifier {
final String uid;
final String name;
final String department;
final String position;
Users({ this.uid, this.department, this.name, this.position });
}
Hope help. Sorry for the english, yo hablo español. :D

Flutter Bloc Equatable state with one mutual property across all states classes

I am trying to figure out the best way to define my Search Bloc's State to preserve text property (search key) across all state classes.
Currently, it looks like this:
import 'package:equatable/equatable.dart';
import 'package:project/models/searchResults.dart';
class SearchState extends Equatable {
SearchState([List props = const []]) : super(props);
}
class SearchStateEmpty extends SearchState {
final String text;
SearchStateEmpty({this.text});
#override
String toString() => 'SearchStateEmpty';
}
class SearchStateLoading extends SearchState {
final String text;
SearchStateLoading({this.text});
#override
String toString() => 'SearchStateLoading';
}
class SearchStateSuccess extends SearchState {
final String text;
final List<RestaurantSearchItem> items;
SearchStateSuccess({this.text, this.items}) : super([text, items]);
#override
String toString() => 'SearchStateSuccess { items: ${items.length} }';
}
class SearchStateError extends SearchState {
final String text;
final String error;
SearchStateError({this.text, this.error}) : super([text, error]);
#override
String toString() => 'SearchStateError';
}
Is there a better way of using text property than defining it throughout all state classes?
This would not be as bad as it is now if I wouldn't have to use currentState property every time an event does not have it. For example:
SearchStateEmpty(text: currentState.text);
...
SearchStateLoading(text: event.text);
...
SearchStateSuccess(text: currentState.text, items: results.items);
I was looking for examples in Flutter docs but all I was able to find out was that I should either use different blocs for it or ditch equatable (which I want to keep since it's pretty nice to have).
Any suggestions with examples would be highly appreciated. Thanks.
The way I was doing it was sort of antipattern. With some help, I was able to find a more clean way to use the current state in my state classes.
Code now looks like this:
class SearchState extends Equatable {
final String text;
SearchState(this.text,
[List<RestaurantSearchItem> items = const [], String error = ''])
: super([text, items, error]);
}
class SearchStateEmpty extends SearchState {
SearchStateEmpty({String text})
: super(text); // Here I want to use text from SearchState
SearchStateEmpty.fromState(SearchState state) : super(state.text);
#override
String toString() => 'SearchStateEmpty';
}
class SearchStateLoading extends SearchState {
final String text;
SearchStateLoading({this.text})
: super(text); // Here I want to set text that comes from event
#override
String toString() => 'SearchStateLoading';
}
class SearchStateError extends SearchState {
final String error;
SearchStateError({this.error, String text}) : super(text, [], error);
// Text comes from SearchState, error comes from event
SearchStateError.fromState(SearchState state, {this.error})
: super(state.text, [], error);
#override
String toString() => 'SearchStateError';
}
class SearchStateSuccess extends SearchState {
final List<RestaurantSearchItem> items;
SearchStateSuccess({this.items, String text}) : super(text, items, null);
SearchStateSuccess.fromState(SearchState state, {this.items})
: super(state.text, items, null);
#override
String toString() => 'SearchStateSuccess { items: ${items.length} }';
}