Passing Map or Class to widget is better practice? - flutter

In my code, I have the Step class:
class Step{
String name;
bool completed;
Step({
#required this.name,
#required this.completed
});
Map<String, dynamic> toMap() => {
'name': name,
'completed': completed
};
}
I use Firebase as my backend and pull data from cloud firestore using StreamBuilder and pass it to my StepView widget:
StreamBuilder(
stream: Firestore.instance.collection('step').snapshots,
builder: (context, snapshot) {
sample_step = snapshot.data.documents[0].data;
return StepView(
step: sample_step
)
}
)
My question is: What is the better practice between two options:
Passing the raw data I pulled from cloud Firestone, which is a Map<String, dynamic>, to my widget:
class StepView extends StatefulWidget {
final Map<String, dynamic> step;
}
Or converting the Map to the Step class and pass that instance of Step class to my widget:
// Convert the Map<String, dynamic> sample_test to class Step
// Need to code the method fromMapToClass in Step class
new_step = Step.fromMapToClass(sample_step)
return StepView(
step: new_step
)
class StepView extends StatefulWidget {
final Step step;
}

That totally depends on how you prefer to code your widgets and which state management scheme you would use.
If I was at your place and I used ProviderModel or any other model for state management:
I would CONVERT the map to class object FIRST
You need to implement a method fromMapToClass in your class to be able to convert the firestore data to the required Step class object.
Your Step class should look somthing like this (the function could change depending upon you data)
class Step {
String name;
bool completed;
Step({#required this.name, #required this.completed});
Map<String, dynamic> toMap() => {'name': name, 'completed': completed};
Step.fromMapToClass(Map<String, dynamic> json) {
this.name = json['name']??"";
this.completed = json['completed']??false;
}
}
Then send the Step class object to my Widget Class (or StateManagement class which in ProviderModel case will be the Provider class that extends ChangeNotifier).
This way your widget will not have to handle the conversion part and can just focus on the design and implementation.

Related

Blocbuilder not updating when list in map changes

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);
....

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.

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

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

How do I get mobx to update when I change a property in an ObservableList?

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

How to avoid repetition in Flutter while using JsonSerialzable

I have models that I need to map them to JSON and vice versa. Therefore, I've followed flutter's JSON and serialization guide on how to do so.
I found myself writing the same code base for each model, like so:
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
part 'folder_entity.g.dart';
#JsonSerializable(explicitToJson: true)
class FolderEntity {
final String id;
final String path;
bool isSelected;
FolderEntity({
#required this.id,
#required this.path,
this.isSelected = false,
});
factory FolderEntity.fromMap(Map<String, dynamic> json) => _$FolderEntityFromJson(json);
Map<String, dynamic> toMap() => _$FolderEntityToJson(this);
}
I thought about moving fromMap and toMap to a new abstract class called Entity, but I'm unable to do so for two reasons:
I can't extend a factory, therefore I have write the same pattern for each method.
I'm not sure if there's an option to change prefix from _$FolderEntity to just _$Entity, and if would still be available, will it even work?
For second question you should be change prefix from _$FolderEntity to just _$Entity in folder_entity.g.dart file.