I build a flutter project by using Bloc State management. But There are something that i don't understand. I know how to make State as Abstract class which it's implement for every it's children.
I have class state lake below, please focus to class MainHomeLoaded
abstract class MainHomeState extends Equatable{
MainHomeState();
}
class MainHomeUnInitialized extends MainHomeState{
#override
List<Object> get props => null;
}
class MainHomeLoading extends MainHomeState{
#override
List<Object> get props => null;
}
class MainHomeLoaded extends MainHomeState{
final List<Article> listArticle;
final bool hasReachedMax;
MainHomeLoaded({#required this.listArticle, this.hasReachedMax});
MainHomeLoaded copyWith({
List<Article> article,
bool hasReacedMax,
}){
return MainHomeLoaded(
listArticle: article ?? this.listArticle,
hasReachedMax: hasReacedMax ?? this.hasReachedMax);
}
#override
List<Object> get props => null;
}
class MainHomeError extends MainHomeState{
final String errorMsg;
MainHomeError({#required this.errorMsg});
#override
List<Object> get props => [errorMsg];
}
then i have MainHomeBloc class with implement Bloc method like mapEventtoState() and inside this method i made conditional like below(again please focus to conditional MainHomeLoaded):
#override
Stream<MainHomeState> mapEventToState(MainHomeEvent event) async*{
if(event is CallHomeLatestNews && !_hasReachedMax(state)){
if(state is MainHomeUnInitialized){
ResponseArticle responseArticle = await mainHomeRepository.latestNews(event.page);
if(responseArticle.status == 'success'){
List<Article> data = responseArticle.data;
yield MainHomeLoaded(listArticle: data);
}else{
yield MainHomeError(errorMsg: responseArticle.message);
}
}
if(state is MainHomeLoaded){
ResponseArticle responseArticle = await mainHomeRepository.latestNews(event.page);
if(responseArticle.status == 'success'){
List<Article> data = responseArticle.data;
yield data.isEmpty ? state.copyWith(hasReacedMax: true)
: MainHomeLoaded(listArticle: state.listArticle + data, hasReachedMax: false);
}
}
}
This is part that i don't understand at all, as you can see we have consider that state are in MainHomeLoaded because inside if conditional, but i got error building and my IDE show red line and also method copyWith()doesn't recognize. The error display like this:
what IDE says is
method copyWith() is not define for the class 'MainHomeState'
Can someone help me to give simple explanation for this case? Thanks
FYI i used Flutter in this version 1.12.13 and Dart version 2.7.0
Finally i found what my main problem. Perhaps it's bit different when we using state in BlocBuilder (in Screens) that automatically known specific state. So what i have to do is just casting it to be child that i wanted. So the solution of this case is like this:
if(state is MainHomeLoaded){
MainHomeLoaded mainHomeLoaded = state as MainHomeLoaded; // what i need
ResponseArticle responseArticle = await mainHomeRepository.latestNews(defaultPage);
if(responseArticle.status == 'success'){
List<Article> newData = responseArticle.data;
if(newData.isEmpty){
mainHomeLoaded.copyWith(hasReacedMax: true);
yield mainHomeLoaded;
}
defaultPage++;
}else{
print('gagal');
yield MainHomeError(errorMsg: responseArticle.message);
}
}
i hope it will help someone in future.
Related
I am a newbie in Flutter and I am trying to build an app using Provider. I will try to provide an oversimplified example here. My app includes a model of a room.
class Room {
String roomDisplayName;
String roomIdentifier;
Image image;
List<IDevices> devices = [];
Room(this.roomDisplayName, this.roomIdentifier, this.image, this.devices);
}
Rooms have list of devices like a temperature sensor
class TempSensor implements IDevices {
late String tempSensorName;
late double temperatureValue;
late double humidityValue;
late int battery;
TempSensor(this.displayName, this.zigbeeFriendlyName);
UpdateTempSensor(double temperature, double humidiy, int battery) {
this.temperatureValue = temperature;
this.humidityValue = humidiy;
this.battery = battery;
}
I have a RoomProvider class that implements ChangeNotifier that is responsible for updating devices in List<Room> rooms
class RoomsRepositoryProvider with ChangeNotifier {
List<Room> get rooms {
//return _rooms;
return _rooms;
}
UpdateTemperatureSensor(TempSensor tempSensor) {
TempSensor? foundTempSensor = null;
_rooms.forEach((room) {
room.devices.forEach((element) {
if (element.displayName == tempSensor.displayName) {
foundTempSensor = element as TempSensor;
}
});
});
if (foundTempSensor != null) {
foundTempSensor?.UpdateTempSensor(tempSensor.temperatureValue,
tempSensor.humidityValue, tempSensor.battery);
notifyListeners();
}
}
I also have a Stateful widget page to show Room information like temperature/humidity value.
class DetailPage extends StatefulWidget {
final Room room;
DetailPage({required this.room});
#override
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
#override
Widget build(BuildContext context) {
context.watch<RoomsRepositoryProvider>().rooms;
return Text ("Temperature is ${widget.room.devices[0].temperatureValue}");
}
Here is question:
The problem I am facing is that, if I am showing the Living Room in DetailPage and the temperature sensor from Bedroom gets updated in the List<Room> rooms, the whole DetailPage gets rebuild. Since it is not an issue in the flutter and the app works good. I would still like to know how to solve this architecture problem, that the DetailPage only gets build for the room updates related to the room being shown?
PS: please ignore any build, indentation or naming convention mistakes.
To only rebuild the specific widget, you can wrap that widget inside Consumer widget provider by Provider in flutter. Consumer takes a builder function and will build the widget returned by this builder function only when the data changes.
Consumer(
builder:(context,_,__){
return Container();
},
),
To implement this, you can use a Comsumer widget
Consumer<RoomsRepositoryProvider>(
builder:(context,value,child) => Text("Temperature is ${value.room}");
),
A StatelessWidget is also sufficient. Don't forget the index by room. It should work like this
So, I solved my problem by creating a separate provider DevicesProvider that contains the list of devices modified in the room. I provide the current room by calling the method SetCurrentRoom(String currentRoomIdentifier) from the DetailPage and the provider does its job whenever the devices list in the current room updates.
class DevicesProvider with ChangeNotifier {
String _currentRoomIdentifier = "";
List<IDevices> _listCurrentRoomDevices = [];
List<IDevices> get ListCurrentRoomDevices => _listCurrentRoomDevices;
void SetCurrentRoom(String currentRoomIdentifier) {
_currentRoomIdentifier = currentRoomIdentifier;
}
UpdateDevicesList(IDevices device) {
if (serviceLocator<RoomProviderService>()
.rooms
.any((room) => room.roomIdentifier == _currentRoomIdentifier)) &&
IsDeviceUpdateComingFromCurrentRoom(device)
{
_listCurrentRoomDevices.clear();
var devices = serviceLocator<RoomProviderService>()
.rooms
.firstWhere(
(room) => room.roomIdentifier == _currentRoomIdentifier)
.devices;
_listCurrentRoomDevices.addAll(devices);
notifyListeners();
}
}
bool IsDeviceUpdateComingFromCurrentRoom(IDevices device) {
bool isUpdateFromCurrentRoom = false;
if (device.Name.contains(_currentRoomIdentifier)) {
isUpdateFromCurrenRoom = true;
}
return isUpdateFromCurrentRoom;
}
}
Maybe this can be solved in a different way which is more elegant or efficient, but for now my problem is solved with this approach.
I'm creating an app with firebase as a database. After sending data to firebase, app screen should pop out for that I had bloclistener on the screen but after sending the data to firestore database, nothing is happening, flow is stopped after coming to loaded state in bloc file why? check my code so that you will know. I can see my data in firebase but it is not popping out because flow is not coming to listener.
state:
class SampletestInitial extends SampletestState {
#override
List<Object> get props => [];
}
class SampletestLoaded extends SampletestState {
SampletestLoaded();
#override
List<Object> get props => [];
}
class SampletestError extends SampletestState {
final error;
SampletestError({required this.error});
#override
List<Object> get props => [error];
}
bloc:
class SampletestBloc extends Bloc<SampletestEvent, SampletestState> {
SampletestBloc() : super(SampletestInitial()) {
on<SampletestPostData>((event, emit) async {
emit(SampletestInitial());
try {
await Repo().sampleTesting(event.des);
emit(SampletestLoaded());
} catch (e) {
emit(SampletestError(error: e.toString()));
print(e);
}
});
}
}
Repo: ---- Firebase post data
Future<void> sampleTesting(String des) async {
final docTicket = FirebaseFirestore.instance.collection('sample').doc();
final json = {'Same': des};
await docTicket.set(json);
}
TicketScreen:
//After clicking the button ---
BlocProvider<SampletestBloc>.value(
value: BlocProvider.of<SampletestBloc>(context, listen: false)
..add(SampletestPostData(description.text)),
child: BlocListener<SampletestBloc, SampletestState>(
listener: (context, state) {
if (state is SampletestLoaded) {
Navigator.pop(context);
print("Popped out");
}
},
),
);
im not sure but i think that you have the same hash of:
AllData? data;
try to remove AllData? data; and create new data variable so you can be sure that you has a new hash code every time you call createTicket method;
final AllData data = await repo.createTicket(AllData(
Check your AllData class properties.
BLoC will not show a new state if it not unique.
You need to check whether all fields of the AllData class are specified in the props field.
And check your BlocProvider. For what you set listen: false ?
BlocProvider.of<SampletestBloc>(context, listen: false)
I am trying to fix an issue related to Flutter Bloc. I am editing someone else code to make it work with the latest flutter_bloc version but I am unable to do so. Can someone do a rewrite for my code so I can run it? I saw many answers but I am unable to understand how to fix my own code.
This is the complete code for all_categories_bloc.dart
class AllCategoriesBloc extends Bloc<AllCategoriesEvent, AllCategoriesState> {
AllCategoriesBloc({
this.apiRepository,
}) : super(AllCategoriesInitial()) {
on<GetAllCategories>(_onGetAllCategories);
}
final ApiRepository apiRepository;
Future<void> _onGetAllCategories(
GetAllCategories event,
Emitter<AllCategoriesState> emit,
) async {
try {
emit(const AllCategoriesLoading());
final categoriesModel = await apiRepository.fetchCategoriesList();
emit(AllCategoriesLoaded(categoriesModel));
if (categoriesModel.error != null) {
emit(AllCategoriesError(categoriesModel.error));
}
} catch (e) {
emit(
const AllCategoriesError(
"Failed to fetch all categories data. Is your device online ?",
),
);
}
}
}
Code for all_categories_event.dart
abstract class AllCategoriesEvent extends Equatable {
AllCategoriesEvent();
}
class GetAllCategories extends AllCategoriesEvent {
#override
List<Object> get props => null;
}
Code for all_categories_state.dart
abstract class AllCategoriesState extends Equatable {
const AllCategoriesState();
}
class AllCategoriesInitial extends AllCategoriesState {
AllCategoriesInitial();
#override
List<Object> get props => [];
}
class AllCategoriesLoading extends AllCategoriesState {
const AllCategoriesLoading();
#override
List<Object> get props => null;
}
class AllCategoriesLoaded extends AllCategoriesState {
final CategoriesModel categoriesModel;
const AllCategoriesLoaded(this.categoriesModel);
#override
List<Object> get props => [categoriesModel];
}
class AllCategoriesError extends AllCategoriesState {
final String message;
const AllCategoriesError(this.message);
#override
List<Object> get props => [message];
}
It throws an error "Bad state: add(GetAllCategories) was called without a registered event handler.
Make sure to register a handler via on((event, emit) {...})"
I have this add(GetAllCategories) in my home. dart file but the solution is to edit this code which I am unable to do so. Can someone do a rewrite for the latest bloc? I would be thankful.
Let's get through the migration guide step by step:
package:bloc v5.0.0: initialState has been removed. For more information check out #1304.
You should simply remove the AllCategoriesState get initialState => AllCategoriesInitial(); portion from your BLoC.
package:bloc v7.2.0 Introduce new on<Event> API. For more information, read the full proposal.
As a part of this migration, the mapEventToState method was removed, each event is registered in the constructor separately with the on<Event> API.
First of all, register your events in the constructor:
AllCategoriesBloc() : super(AllCategoriesInitial()) {
on<GetAllCategories>(_onGetAllCategories);
}
Then, create the _onGetAllCategories method:
Future<void> _onGetAllCategories(
GetAllCategories event,
Emitter<AllCategoriesState> emit,
) async {
try {
emit(const AllCategoriesLoading());
final categoriesModel = await _apiRepository.fetchCategoriesList();
emit(AllCategoriesLoaded(categoriesModel));
if (categoriesModel.error != null) {
emit(AllCategoriesError(categoriesModel.error));
}
} catch (e) {
emit(
const AllCategoriesError(
"Failed to fetch all categories data. Is your device online ?",
),
);
}
}
Notice, that instead of using generators and yielding the next state, you should use the Emitter<AllCategoriesState> emitter.
Here is the final result of the migrated AllCategoriesBloc:
class AllCategoriesBloc extends Bloc<AllCategoriesEvent, AllCategoriesState> {
AllCategoriesBloc() : super(AllCategoriesInitial()) {
on<GetAllCategories>(_onGetAllCategories);
}
final ApiRepository _apiRepository = ApiRepository();
Future<void> _onGetAllCategories(
GetAllCategories event,
Emitter<AllCategoriesState> emit,
) async {
try {
emit(const AllCategoriesLoading());
final categoriesModel = await _apiRepository.fetchCategoriesList();
emit(AllCategoriesLoaded(categoriesModel));
if (categoriesModel.error != null) {
emit(AllCategoriesError(categoriesModel.error));
}
} catch (e) {
emit(
const AllCategoriesError(
"Failed to fetch all categories data. Is your device online ?",
),
);
}
}
}
Bonus tip
Instead of creating an instance of ApiRepository inside the BLoC directly, you can use the constructor injection:
class AllCategoriesBloc extends Bloc<AllCategoriesEvent, AllCategoriesState> {
AllCategoriesBloc({
required this.apiRepository,
}) : super(AllCategoriesInitial()) {
on<GetAllCategories>(_onGetAllCategories);
}
final ApiRepository apiRepository;
...
}
Now, when creating BLoC, pass the instance of the repository to the constructor, like AllCategoriesBloc(apiRepository: ApiRepository()). This way you will be able to properly unit test your BLoC by mocking dependencies (in this case, ApiRepository).
everyone. I am new in Flutter and BLoC pattern.
I needed to implement contact page so I created event GetContacts and passed it into context.read().add() after that I called this event into initState() of contacts screen.
Here my event:
abstract class ContactEvent extends Equatable {
const ContactEvent([List props = const []]) : super();
}
class GetContacts extends ContactEvent {
const GetContacts() : super();
#override
List<Object> get props => [];
}
Here is my bloc:
class ContactsBloc extends Bloc<ContactEvent, ContactsState> {
final ContactsRepo contactsRepo;
ContactsBloc({required this.contactsRepo}) : super(ContactInitial());
#override
Stream<ContactsState> mapEventToState(ContactEvent event,) async* {
yield ContactsLoading();
//
// if (event is UpdatePhoto) {
// yield PhotoLoading();
//
// print("LOADING STARTED");
//
// final photo = await contactsRepo.updatePhoto(event.identifier, event.photo);
// print("LOADING FINISHED");
//
// yield PhotoLoaded(photo: photo);
// }
if (event is GetContacts) {
print("get contacts photoBloc");
try {
final contacts = await contactsRepo.getContacts();
yield ContactsLoaded(contacts);
} on AccessException {
yield ContactsError();
}
}
}
}
That works right and contacts page renders contacts as it is supposed.
[contacts screen][1]
[1]: https://i.stack.imgur.com/Gx3JA.png
But then I decided to implement new feature: when user clicks on any contact he is offered to change its photo.
If I understand BLoC pattern correctly then if I want to change my state I need to create new event. Then I created new action UpdatePhoto and passed it into the same Bloc as it shown at 2nd part of code (in comments). Exactly there I encounter a misunderstanding of architecture expansion. This action is not supposed to return ContactsLoaded state so when I tried to catch this into my another bloc builder it broke my previous bloc builder that caught GetContact event.
ContactState:
abstract class ContactsState extends Equatable {
const ContactsState([List props = const []]) : super();
}
// class PhotoLoading extends PhotoState {
// #override
// List<Object?> get props => [];
// }
//
// class PhotoLoaded extends PhotoState {
// final Uint8List photo;
// const PhotoLoaded({required this.photo});
// #override
// List<Object?> get props => [photo];
// }
class ContactInitial extends ContactsState {
#override
List<Object> get props => [];
}
class ContactsLoading extends ContactsState {
#override
List<Object> get props => [];
}
class ContactsLoaded extends ContactsState {
final List<MyContact> contacts;
ContactsLoaded(this.contacts) : super([contacts]);
#override
List<Object> get props => [contacts];
}
class ContactsError extends ContactsState {
#override
List<Object?> get props => [];
}
Question: If I want to create new event (for example UpdatePhoto) which is not supposed to return the state that I caught before at the same bloc then I need to create new bloc for that purpose and cover my screen by multiProvider?
You should also post your ContactState code.
However you do not necessarely need a new Bloc. It all depends on what you are trying to achieve.
I suppose than when you yield PhotoLoading() you want to show a loader.
But when you update the photos, if I understand what you are trying to achieve you should yield an updated list of contacts using again yield ContactsLoaded(contacts) or add(GetContacts())instead of yield PhotoLoaded(photo: photo).
If you want to show a confirmation message, you can keep your PhotoLoaded state, but you need to build your UI taking into account the different state the bloc may emit.
Remember in BloC architecture event can yield to multiple states in successions and the UI decide if and how to react to each state.
I guess use optional parameter buildWhen in BlocBuilder is the best way to avoid creating new bloc for each event.
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],
);