since one cannot extend an extension of a class such as class MyBloc extends Bloc<MyEvent, MyState>, what is a cleanest way to get some functions out of my bloc file? Since the logic is a bit more complex, I would like to get some sub functions out like in
#override
Stream<MyState> mapEventToState(MyEvent event) async* {
yield* event.map(
loadRequested: (e) => _mapLoadRequestedToState(),
dataEntered: (e) => _mapDataEnteredToState(),
);
}
Stream<LogicChainState> _mapLoadRequestedToState() async* {
final dataRaw = loadData();
final dataProc = initData(dataRaw);
yield doSomeMore(dataProc);
}
I don't like the idea of using global functions. I could create a class
class MyBlocUtils {
MyData initData(MyData dataRaw) {
...
}
MyData doSomeMore(MyData dataProc) {
...
}
}
which still isn't as nice as using a function defined within the class MyBloc.
Any advice?
If all you want is to separate your methods into multiple files, but keep them in the same class, you could use extension methods.
my_bloc.dart
part 'my_bloc_utils.dart';
class MyBloc extends Bloc<MyEvent, MyState> {
Stream<LogicChainState> _mapLoadRequestedToState() async* {
final dataRaw = loadData();
final dataProc = initData(dataRaw);
yield doSomeMore(dataProc);
}
}
my_bloc_utils.dart
part of 'my_bloc.dart';
extension MyBlocUtils on MyBloc {
#override
Stream<MyState> mapEventToState(MyEvent event) async* {
yield* event.map(
loadRequested: (e) => _mapLoadRequestedToState(),
dataEntered: (e) => _mapDataEnteredToState(),
);
}
}
You can access the methods in just the same way as you keep everything in a single file:
import 'my_bloc.dart';
final myBloc = MyBloc();
final stream = myBloc.mapEventToState(MyEvent());
Related
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).
I am new in this state management world. So I was trying to follow this tutorial (How to Save Products in a Wishlist using the BloC Pattern - EP10 - The eCommerce Series), but mapEventToState is deprecated so I am not sure what to do.
Here is my state:
part of 'wishlist_bloc.dart';
abstract class WishlistState extends Equatable {
const WishlistState();
#override
List<Object> get props => [];
}
class WishlistLoading extends WishlistState {}
class WishlistLoaded extends WishlistState {
final WishlistModel wishlist;
const WishlistLoaded({this.wishlist = const WishlistModel()});
#override
List<Object> get props => [wishlist];
}
class WishlistError extends WishlistState {}
Here is my event:
part of 'wishlist_bloc.dart';
abstract class WishlistEvent extends Equatable {
const WishlistEvent();
#override
List<Object> get props => [];
}
class StartWishlist extends WishlistEvent {}
class AddWishlistProduct extends WishlistEvent {
final ProductModel product;
const AddWishlistProduct(this.product);
#override
List<Object> get props => [product];
}
class RemoveWishlistProduct extends WishlistEvent {
final ProductModel product;
const RemoveWishlistProduct(this.product);
#override
List<Object> get props => [product];
}
Here is my bloc:
import '../models/product_model.dart';
import '../models/wishlist_model.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
part 'wishlist_event.dart';
part 'wishlist_state.dart';
class WishlistBloc extends Bloc<WishlistEvent, WishlistState> {
WishlistBloc() : super(WishlistLoading()) {
on<StartWishlist>(_mapStartWishlistToState);
on<AddWishlistProduct>(_mapAddWishlistToState);
on<RemoveWishlistProduct>(_mapRemoveWishlistToState);
}
void _mapStartWishlistToState(event, emit) async {
emit(WishlistLoading());
try {
await Future.delayed(Duration(seconds: 1));
emit(WishlistLoaded());
} catch (_) {}
}
// Error ...
void _mapAddWishlistToState(event, emit) async {
if (state is WishlistLoaded) {
try {
emit(WishlistLoaded(
wishlist: WishlistModel(
products: List.from(state.wishlist.products)
..add(event.product))));
} catch (_) {}
}
}
void _mapRemoveWishlistToState(event, emit) async {}
}
But I get this error instead: "The getter 'wishlist' isn't defined for the type 'WishlistState'. Try importing the library that defines 'wishlist', correcting the name to the name of an existing getter, or defining a getter or field name 'wishlist'".
How to access 'wishlist' in the new version of flutter_bloc? Thank you.
Try to define arguments types while difining the functions. So your updated code would look like this:
import '../models/product_model.dart';
import '../models/wishlist_model.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
part 'wishlist_event.dart';
part 'wishlist_state.dart';
class WishlistBloc extends Bloc<WishlistEvent, WishlistState> {
WishlistBloc() : super(WishlistLoading()) {
on<StartWishlist>(_mapStartWishlistToState);
on<AddWishlistProduct>(_mapAddWishlistToState);
on<RemoveWishlistProduct>(_mapRemoveWishlistToState);
}
void _mapStartWishlistToState(
// Added argument types below
StartWishlist event, Emitter<WishlistState> emit) async {
emit(WishlistLoading());
try {
await Future.delayed(Duration(seconds: 1));
emit(WishlistLoaded());
} catch (_) {}
}
void _mapAddWishlistToState(
// Added argument types below
AddWishlistProduct event, Emitter<WishlistState> emit) async {
if (state is WishlistLoaded) {
try {
emit(WishlistLoaded(
wishlist: WishlistModel(
products: List.from(state.wishlist.products)
..add(event.product))));
} catch (_) {}
}
}
void _mapRemoveWishlistToState(
// Added argument types below
RemoveWishlistProduct event, Emitter<WishlistState> emit) async {}
}
Type promotion is impossible for properties, since they can potentially return different values each time they are called. As such, it is impossible for the compiler to know that the state getter will return a WishlistLoaded instance, even after knowing that the same getter returned a WishlistLoaded four lines earlier.
One way around this is to assign the state to a local variable, which is eligible for type promotion.
void _mapAddWishlistToState(AddWishlistProduct event, Emitter<WishlistState> emit) async {
final state = this.state; // local variable
if (state is WishlistLoaded) {
try {
emit(WishlistLoaded(
wishlist: WishlistModel(
products: List.from(state.wishlist.products)
..add(event.product))));
} catch (_) {}
}
}
The linked video used a parameter, which is also eligible for type promotion. If it had used the state getter directly in _mapAddWishlistProductToState, it would have run into the same error.
You just need to cast your state as following
void _mapAddWishlistToState(event, emit) async {
if (state is WishlistLoaded) {
try {
emit(WishlistLoaded(
wishlist: WishlistModel(
products: List.from((state as WishlistLoaded).wishlist.products)
..add(event.product))));
} catch (_) {}
}
}
I am trying to find solution to manage async queries. For example, i have internet shop and i want to update all products and categories when city is changed. And i don't want to keep all async logic on ui. In order to achieve this result, i've created this bloc:
class AppEvent {
String message;
AppEvent({this.message = ''});
}
class EventsBlock extends Bloc<AppEvent, AppEvent> {
EventsBlock() : super(AppEvent());
#override
Stream<AppEvent> mapEventToState(AppEvent event) async* {
yield event;
}
}
final events = EventsBlock();
Then, i can use it like this:
class CityCubit() {
CityCubit() : super(CityState());
Future<void> changeCity() async {
await api.changeCity();
events.add(AppEvent(message: 'cityChanged'));
}
}
class CategoryCubit extends Cubit<CategoryState> {
CategoryCubit() : super(CategoryEmptyState()) {
events.stream.listen((e) {
if(e.message == 'cityChanged') {
fetchCategories();
}
});
};
Future<void> fetchCategories() async {
//fetch categories
}
}
class ProductCubit extends Cubit<ProductState> {
ProductCubit() : super(ProductEmptyState()) {
events.stream.listen((e) {
if(e.message == 'cityChanged') {
fetchProducts();
}
});
};
Future<void> fetchProducts() async {
//fetch products
}
}
It's something like eventBus pattern. But i am not sure that it's a correct way to use bloc. I've tried to use redux + redux saga, but it has a lot of boilerplate and i believe that flutter has better solution to manage things like that.
Your general idea is ok, but I can't see a real need for the EventsBloc class. In fact, it is kinda strange that you use the same class for the events and for the states of this bloc, and simply yield the event you receive. It's like EventsBloc could be a simple stream.
Here's a way to go, turning CityCubit into an actual bloc (and also with some error handling, which is something you can do gracefully with bloc):
abstract class CityState {}
class CityInitial extends CityState {}
class CityLoadingCities extends CityState {}
class CityCitiesLoaded extends CityState {}
class CityLoadError extends CityState {}
abstract class CityEvent {}
class CityLoadCities extends CityEvent {}
class CityBloc<CityEvent, CityState> {
CityBloc() : super(CityInitial());
#override
Stream<AppEvent> mapEventToState(CityEvent event) async* {
if(event is CityLoadCities) {
yield CityLoadingCities();
try {
await api.changeCity();
yield CityCitiesLoaded();
} catch (error) {
yield CityLoadError();
}
}
}
void changeCity() {
add(CityLoadCities());
}
}
Now you can do this inside any other bloc:
instanceOfCityBloc.listen((cityState) {
if(cityState is CityCitiesLoaded){
// do stuff
}
});
I ended up with this code:
class CityChangedEvent {
int cityId;
CityChangedEvent(this.cityId);
}
EventBus eventBus = EventBus();
mixin EventBusMixin {
StreamSubscription<T> listenEvent<T>(void Function(T) subscription) =>
eventBus.on<T>().listen(subscription);
void shareEvent<S>(S event) => eventBus.fire(event);
}
class CityCubit extends CityCubit<CityState> with EventBusMixin {
CityCubit() : super(CityInitialState());
Future<void> changeCity(cityId) async {
try {
emit(ChangeCityLoadingState());
final result = await api.changeCity(cityId);
if(result.success) {
emit(ChangeCitySuccessState());
shareEvent(CityChangedEvent(cityId));
}
} catch (_) {
emit(ChangeCityErrorState());
}
}
}
class CategoryCubit extends Cubit<CategoryState> with EventBusMixin {
CategoryCubit() : super(CategoryEmptyState()) {
listenEvent<CityChangedEvent>((e) {
fetchCategories(e.cityId);
);
}
Future<void> fetchCategories(cityId) async {
try {
emit(CategoryLoadingState());
final categoriesList = await fetchCategoriesApi();
emit(CategoryLoadedState(categories: categoriesList));
} catch (_) {
emit(CategoryErrorState());
}
}
}
Now, i can communicate between blocs without the need to instantiate or inject their instances. Thanks to this library https://pub.dev/packages/event_bus
Consider an app for counting colors.
A server provides a list of colors.
The user can click on a color in the app UI
The clicks per color are counted and each click is stored on the server.
I have build a BLoC to manage the "color-counters".
class ColorsBloc extends Bloc<ColorsEvent, ColorsState> {
final ColorRepository colorRepository;
ColorsBloc({required this.colorRepository}) : super(ColorsState.initial());
#override
Stream<ColorsState> mapEventToState(
ColorsEvent event,
) async* {
if (event is ColorsFetchRequested) {
yield ColorsState.loading();
try {
final colors = await colorRepository.getColors();
yield ColorsState.success(colors);
} catch (e) {
yield ColorsState.error();
}
} else if (event is ColorCounted) {
yield* _mapColorCountedToState(event);
}
}
Stream<ColorsState> _mapColorCountedToState(ColorCounted event) async* {
yield state.copyWith(
sendingByColorId: {...state.sendingByColorId, event.colorId},
);
await colorRepository.storeColor(Color(
colorId: event.colorId,
timestamp: DateTime.now().millisecondsSinceEpoch,
));
final colors = await colorRepository.getColors();
yield state.copyWith(
status: Status.success,
colors: colors,
sendingByColorId: {...state.sendingByColorId}..remove(event.colorId),
);
}
}
Sending a color-click takes some time (let's say 1 second on a slow network). The user may not click a color again before it is stored to the server (what the sendingByColorId set keeps track of).
PROBLEM
The user however may click on different colors very fast. The counters are working in that case, but they lag behind because events are processed FIFO (including the await colorRepository.storeColor(...) and the await to get the updated colors list).
I want the sending state to update immediately after any click even if there are previous clicks which are currently in the process of storing it to the repository.
How can I enable the BLoC to keep on processing new events while another one is awaiting the API response?
Notice the main idea of using Bloc is predictability - you will lose that predictability to some degree (depending on your concrete implementation). If you are using flutter_bloc you could follow this suggestion and override the default event stream processing on your bloc.
#override
Stream<Transition<MyEvent, MyState>> transformEvents(
Stream<MyEvent> events, transitionFn) {
return events.flatMap(transitionFn);
}
You could also look into isolates and maybe especially flutters compute which lets you spin up an isolate to run your code. I found this to be a good source.
While I'm very sure there is a better way to do this I came up with the following. I've cut out some of your logic for it to be a little more generic.
I'm not familiar with the performance details of compute and isolate in dart, so I want to make the disclaimer that this might not be a best practice approach, but maybe it helps you getting started.
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(ExampleApp());
}
class ExampleApp extends StatelessWidget {
static ExampleBloc bloc = ExampleBloc();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: TextButton(
onPressed: () => bloc.add(ExampleStartingEvent()),
child: Text("Trigger"),
),
);
}
}
// Top level function that is computed in isolate
Future<void> _mockRequest(String body) async {
// Do your async request here and await response
Future.delayed(Duration(seconds: 5));
ExampleBloc.successfulCompute("Successful!");
}
// Bloc
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
ExampleBloc() : super(ExampleStateInitial());
static successfulCompute(String response) {
ExampleApp.bloc.add(ExampleEventSuccess(response));
}
#override
Stream<ExampleState> mapEventToState(
ExampleEvent event,
) async* {
if (event is ExampleEventSuccess) {
print(event.response);
yield ExampleStateSuccess(event.response);
}
if (event is ExampleStartingEvent) {
compute(_mockRequest, "body");
}
}
}
// Events
class ExampleEvent {}
class ExampleStartingEvent extends ExampleEvent {}
class ExampleEventSuccess extends ExampleEvent {
final response;
ExampleEventSuccess(this.response);
}
// States
class ExampleState {}
class ExampleStateInitial extends ExampleState {}
class ExampleStateSuccess extends ExampleState {
final response;
ExampleStateSuccess(this.response);
}
class ExampleStateError extends ExampleState {}
Just to show a solution based on #kohjakob 's proposal but with:
no static methods
complete error handling routines
The idea is basically to wrap the repository call into an async method (_sendClick(...)) and call it non-blocking (i.e. without await) while the status update on the sending state is done synchronously.
The _sendClick(...) waits for the repository and adds a ColorSendSuccess or ColorSendFailed event to the bloc once it's done. These events are then handle in their own run of the mapEventToState(...) routine.
class ColorsBloc extends Bloc<ColorsEvent, ColorsState> {
final ColorRepository colorRepository;
ColorsBloc({required this.colorRepository}) : super(ColorsState.initial());
#override
Stream<ColorsState> mapEventToState(
ColorsEvent event,
) async* {
if (event is ColorsFetchRequested) {
yield ColorsState.loading();
try {
final colors = await colorRepository.getColors();
yield ColorsState.success(colors);
} catch (e) {
yield ColorsState.error();
}
} else if (event is ColorCounted) {
yield* _mapColorCountedToState(event);
} else if (event is ColorSendSuccess) {
yield _mapColorSendSuccessToState(event);
} else if (event is ColorSendFailed) {
yield _mapColorSendFailedToState(event);
}
}
Stream<ColorsState> _mapColorCountedToState(ColorCounted event) async* {
yield state.copyWith(
sendingByColorId: {...state.sendingByColorId, event.colorId},
);
// non-blocking <----------------
_sendClick(Color(
colorId: event.colorId,
timestamp: DateTime.now().millisecondsSinceEpoch,
));
final colors = await colorRepository.getColors();
yield state.copyWith(
status: Status.success,
colors: colors,
sendingByColorId: {...state.sendingByColorId}..remove(event.colorId),
);
}
Future<void> _sendClick(Color color) async {
try {
int newId = await colorRepository.storeColor(color);
Color storedColor = color.copyWith(id: () => newId);
add(ColorSendSuccess(color: storedColor));
} on StoreColorClickException catch (_) {
add(ColorSendFailed(color: color));
}
}
ColorsState _mapColorSendSuccessToState(ColorCounted event) async* {
return state.copyWith(
colors: [...state.colors]
// replace the local color-click with the stored one
..removeWhere((element) => element.localId == event.color.localId)
..add(event.color.copyWith(localId: () => null)),
sendingByColorId: {...state.sendingByColorId}..remove(event.color.id),
);
}
ColorsState _mapColorSendFailedToState(ColorCounted event) async* {
return state.copyWith(
colors: [...state.colors]
// remove the color that has not been stored
..removeWhere((element) => element.localId == event.color.localId),
sendingByColorId: {...state.sendingByColorId}..remove(event.color.localId),
// mark the color as failed
errorByColorId: {...state.errorByColorId, event.color.localId},
);
}
}
How can I call the method of one StateNotifier from another StateNotifier? I want to call addNewHabit (in the top class) from submitData (in the lower class).
Here are the bodies of the classes:
class HabitStateNotifier extends StateNotifier<List<Habit>> {
HabitStateNotifier(state) : super(state ?? []);
void startAddNewHabit(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (_) {
return NewHabit();
});
}
//this one right here
void addNewHabit(String title) {
final newHabit = Habit(title: title);
state.add(newHabit);
}
void deleteHabit(String id) {
state.removeWhere((habit) => habit.id == id);
}
}
and
class TitleControllerStateNotifier
extends StateNotifier<TextEditingController> {
TitleControllerStateNotifier(state) : super(state);
void submitData() {
if (state.text.isEmpty) {
return;
} else {
//call 'addNewHabit' from above class
}
}
}
What is the correct way to do this?
Technically, not a recommended pattern to use as StateNotifiers are controllers and you shouldn't really be calling controllers inside controllers.
But this is pretty easy to accomplish as you can read other providers inside a provider.
final habitProvider = StateNotifierProvider((ref) => HabitStateNotifier());
final userControllerProvider = StateNotifierProvider((ref) {
return UserController(
habitProvider : ref.read(habitProvider),
);
});
And then use the reference and call
class TitleControllerStateNotifier
extends StateNotifier<TextEditingController> {
TitleControllerStateNotifier(state, HabitStateNotifier habitProvider) :
habit = habitProvider,
super(state);
final HabitStateNotifier habit;
void submitData() {
if (state.text.isEmpty) {
return;
} else {
habit.addNewHabit(state.text);
}
}
}
Set up a StateNotifierProvider (from Riverpod), which will give you back a StateNotifier after running the create callback. This callback has a (ref) parameter on which you can call ref.read and ref.watch to fetch other providers in either non-depending or depending mode.