I'm trying to trigger an update when a list in my map changes. Type is Map<String, List<int>>. Basically one of the integers is changing in the list but not triggering the blocbuilder. Although when I print the state the value is updated. I'm using freezed. From what I understand freezed only provides deep copies for nested #freezed objects but not for Iterables. I've seen a few solutions for this kind of problem. For example create a new Map with Map.from and emit that map. But that doesn't trigger a rebuild. Any suggestions!
My freezed state is
onst factory RiskAttitudeState.loaded({
required int customerId,
required RiskAttitudeQuestionsInfo riskAttitude,
required Map<String, List<int>> answerIds,
#Default(FormzStatus.pure) FormzStatus status,
int? finalRisk,
}) = RiskAttitudeLoaded;
And I'm updating an integer in the list type List<int> in the map answerIds
Here is the bloc
Future _mapAnswerToState(
String id, List<int> answerIds, Emitter<RiskAttitudeState> emit) async {
await state.maybeMap(
loaded: (RiskAttitudeLoaded loaded) async {
if (loaded.answerIds.containsKey(id)) {
loaded.answerIds.update(
id,
(_) => answerIds,
ifAbsent: () {
add(RiskAttitudeEvent.error(Exception('unknown Question ID: $id')));
return answerIds;
},
);
}
emit(loaded.copyWith(answerIds: loaded.answerIds));
},
orElse: () async {},
);
}
For contest if I pass an empty map like this emit(loaded.copyWith(answerIds:{}));
the builder gets triggered.
unfortunality i came accross this problem too. if your algorithm requires change one item of list maybe you can remove this item from your list and then change its properties. after that if you add the item to the list, builder will be triggered..
I tried a small code with cubit and Equatable and it worked. the key note is that you should override props method and add answerIds and other fields if exists to props and all fields must be final.
also notice to use Map<String, List<int>>.from to fill the map.
so the state class looks like this:
class UcHandleState extends Equatable {
final Map<String, List<int>> answerIds;
const UcHandleState({
required this.answerIds,
});
#override
List<Object> get props => [
answerIds,
];
UcHandleState copyWith({
Map<String, List<int>>? answerIds,
}) {
return UcHandleState(
answerIds: answerIds != null
? Map<String, List<int>>.from(answerIds)
: this.answerIds,
);
}
}
and a simple cubit class for managing events is like below. in valueChanged I'm just passing List<int>.
class TestCubit extends Cubit<TestState> {
TestCubit() : super(const TestState(answerIds: {'1': [1, 1]}));
void valueChanged(List<int> newValues ) {
Map<String, List<int>> test = Map<String, List<int>>.from(state.answerIds);
test['1'] = newValues;
emit(state.copyWith(
answerIds: test,
));
}
}
so in UI I call valueChanged() method of cubit:
cubit.valueChanged(newValues:[ Random().nextInt(50), Random().nextInt(70)]);
and the blocBuilder gets triggered:
return BlocBuilder<UcHandleCubit, UcHandleState>(
buildWhen: (prev, cur) =>
prev.answerIds!= cur.answerIds,
builder: (context, state) {
print(state.answerIds.values);
....
Related
I've got a Stream<UserProfile> being returned form a firebase service.
I'm using MVVM architecture and have a ProfileViewModel which is extended by a freezed state class:
class ProfileModel extends StateNotifier<ProfileState> {
ProfileModel({
required this.authService,
required this.databaseService,
}) : super(const ProfileState.loading());
late AuthService authService;
late FirestoreDatabase databaseService;
Stream<UserProfile?> get userProfile {
return databaseService.profileStream();
}
}
The above results in the following view:
final profileModelProvider =
StateNotifierProvider.autoDispose<ProfileModel, ProfileState>((ref) {
final authService = ref.watch(authServiceProvider);
final databaseService = ref.watch(databaseProvider)!;
return ProfileModel(
authService: authService, databaseService: databaseService);
});
class ProfilePageBuilder extends ConsumerWidget {
const ProfilePageBuilder({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(profileModelProvider);
final model = ref.read(profileModelProvider.notifier);
final up = ref.read(userProfileProvider);
return ProfilePage(
onSubmit: () => {},
name: up.value?.uid ?? "Empty",
canSubmit: state.maybeWhen(
canSubmit: () => true,
success: () => true,
orElse: () => false,
),
isLoading: state.maybeWhen(
loading: () => true,
orElse: () => false,
),
errorText: state.maybeWhen(
error: (error) => error,
orElse: () => null,
),
);
}
}
I would like to know the correct way (using riverpod) to pass the firebase stream to the UI without mixing up UI/BL without loosing functionality of real time data.
I was able to create a StreamProvider which referenced the profile model but it doesnt feel right.
final userProfileProvider = StreamProvider.autoDispose<UserProfile?>((ref) {
return ref.watch(profileModelProvider.notifier).userProfile;
});
My alternative is to convert streams to futures within the view model and then update the state as the function runs.
I'm really quite stuck here, any help would be appreciated
My guess is you want to
listen to a stream from Firebase
When the latest value changes, you want any dependencies to update
You only want the latest value of the stream.
INTRODUCING BehaviorSubject!
You'll need package:rxdart though you may already have it installed.
import 'package:rxdart/rxdart.dart';
#riverpod
BehaviorSubject<ProfileState> userProfileSubject(
UserProfileSubjectRef ref) {
final stream = ....;
// Get the stream and wrap it in a BehaviorSubject
return BehaviorSubject()..addStream(stream);
}
#riverpod
UserProfile? userProfile(
UserProfileRef ref) {
final behaviorSubject = ref.watch(userProfileSubjectProvider);
// when the underlying stream updates,
// invalidate self so we read the new value
behaviorSubject.doOnData((newProfileState) { ref.invalidateSelf(); });
// note that value could be null while stream
// emits a value. You can wait for that
// and convert this provider to return a Future<UserProfile>
// or in the UI handle the null.
// note that firebase could also have a null value.
return behaviorSubject.value?.userProfile;
}
I am using model to convert data to map. When I pass value to the model from class A it returns a map and then I am passing the returned value (Map) to class B. Before sending the valuing I am printing value in class A (It is showing data) but in class B it is showing null. here is class A function.
///Dummy Data remove later
jobPostModel = JobPostModel(
jobPost: 'Web design',
);
var data = jobPostModel.toJob(jobPostModel);
print('before $data');
JobPostScreen8(data);
Here is Model class function which is returning Map
class JobPostModel {
String jobPost;
JobPostModel(
{this.jobPost});
Map toJob(JobPostModel info){
var data = Map<String, dynamic>();
data['jobPost'] = info.jobPost;
return data;
}
factory JobPostModel.fromJob(Map<String, dynamic> data){
return JobPostModel(
jobPost: data['jobPost']
);
}
}
and here is other class where i must display data but it is showing null
class JobPostScreen8 extends StatefulWidget {
Map jobPostModelMapData;
JobPostScreen8([this.jobPostModelMapData]);
#override
_JobPostScreen8State createState() => _JobPostScreen8State();
}
class _JobPostScreen8State extends State<JobPostScreen8> {
#override
void initState() {
super.initState();
print('after ${widget.jobPostModelMapData}');
}
}
The solution is I was passing directly to class B. All I need is to use Navigation.
BlocListener<JobBloc, JobState>(
listener: (context, state) {
if (state is JobSuccessfulState)
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => JobPostScreen8(state.data)));
})
here is Bloc
if (event is PreviewPostJob) {
yield JobSuccessfulState(_updateModel());
}
and state
class JobSuccessfulState extends JobState {
var data;
JobSuccessfulState([this.data]);
}
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 initializing the data in my provider in didChangeDependencies in the parent widget.
#override
void didChangeDependencies() {
super.didChangeDependencies();
final provider = Provider.of<NewArrivalsProvider>(context);
FirebaseFirestore.instance.collection(CurrentUser.getCurrentUser().uid).doc('newArrivals').get().then(
(snapshot) {
Map<String, dynamic> data = snapshot.data();
provider.init(data);
},
);
}
Then updating the data in the child widget.
The change in the data is does not persist however.
Switch(
value: item.value,
onChanged: (state) => provider.update(key: item.key, state: state),
)
Only one switch changes value at a time.
class NewArrivalsProvider extends ChangeNotifier {
Map<String, dynamic> _items = {};
Map<String, dynamic> get items => _items;
int get length => _items.length;
void init(Map<String, dynamic> data) {
_items = data['mapUrls'];
}
void update({#required String key, #required bool state}) {
_items.update(key, (value) => value = state);
notifyListeners();
}
}
Since this is the first I used Provider in the didChangeDependencies method, I'm suspecting that's where the error is. Am I correct?
I solved it.
I changed the provider declaration in the didChangeDependencies to
final provider = Provider.of<NewArrivalsProvider>(context, listen: false);
When listen: true, this line listens to the changes I make to the data.
Then it downloads the data again from Firestore (which is set to false initially)
The change to listen: false makes line run only when the user navigates to the page and does not update every time I change the data
I currently have been using mobx for my flutter app, and I'm trying to update a ListTile to change it's colour onTap. Right now I have I have an ObservableList marked with #observable, and an #action that changes a property on an item in that list.
class TestStore = TestStoreBase with _$TestStore;
abstract class TestStoreBase with Store {
final DataService _dataService;
TestStoreBase({
#required DataService dataService,
}) : assert(dataService != null),
_dataService = dataService,
players = ObservableList<Player>();
#observable
ObservableList<Player> players;
#action
Future<void> loadPlayers(User user) async {
final userPlayers = await _dataService.getUserPlayers(user);
players.addAll(userPlayers);
}
#action
void selectPlayer(int index) {
players[index].isSelected = !players[index].isSelected;
);
}
}
in my UI I have this inside of a listbuilder:
return Observer(builder: (_) {
return Container(
color: widget.testStore.players[index].isSelected != null &&
widget.testStore.players[index].isSelected
? Colors.pink
: Colors.transparent,
child: ListTile(
leading: Text(widget.testStore.players[index].id),
onTap: () => widget.testStore.selectPlayer(index),
),
);
});
but it doesn't redraw when I call widget.testStore.selectPlayer(index);
The second thing I tried was to add #observable in the 'Players' class on the isSelected bool, but it doesn't seem to work either.
#JsonSerializable()
class Player {
final String id;
final bool isUser;
#observable
bool isSelected;
Player(this.id, this.isUser, this.isSelected);
factory Player.fromJson(Map<String, dynamic> data) => _$PlayerFromJson(data);
Map<String, dynamic> toJson() => _$PlayerToJson(this);
}
any help would be greatly appreciated, thanks!
Your are trying to take actions on the isSelected property, so basically you have to define the Player class as a MobX store as well to create a mixin that triggers reportWrite() on modifying isSelected.
Adding #observable annotation to players property only means to watch on the property itself, and typing players as a ObservableList means to watch on the list elements of the property, i.e. to watch on players[0], players[1]...and so on.
For example
#JsonSerializable()
class Player = _Player with _$Player;
abstract class _Player with Store {
final String id;
final bool isUser;
#observable
bool isSelected;
_Player(this.id, this.isUser, this.isSelected);
factory _Player.fromJson(Map<String, dynamic> data) => _$PlayerFromJson(data);
Map<String, dynamic> toJson() => _$PlayerToJson(this);
}
Here is a similar issue from MobX's GitHub repo: https://github.com/mobxjs/mobx.dart/issues/129