How do I cancel a StreamSubscription inside a Cubit? - flutter

I have a cubit that listens to a stream of messages, and emits a state which holds the messages.
In the screen, I am using a BlocProvider to access the cubit, and a BlocBuilder to display
the messages.
In instances like below, do I need to close the StreamSubscription created on listen()? Is there a clean way to do it?
class MessageCubit extends Cubit<MessageState> {
final GetMessagesUseCase getMessagesUseCase;
MessageCubit({this.getMessagesUseCase}) : super(MessageInitial());
Future<void> getMessages({String senderId, String recipientId}) async {
emit(MessageLoading());
try {
final messagesStreamData = getMessagesUseCase.call();
//This is where I listen to a stream
messagesStreamData.listen((messages) {
emit(MessageLoaded(messages: messages));
});
} on SocketException catch (_) {
emit(MessageFailure());
} catch (_) {
emit(MessageFailure());
}
}
}

You don't need to close the subscription, but you should as good practice to avoid potential memory leaks. Since it is so straightforward it's not any sacrifice.
Create a class variable of type StreamSubscription<your type>. Let's say it's named sub.
In getMessages before listen: await sub?.cancel()
Then sub = messagesStreamData.listen(...
Override the Cubit's close method and run the same command as in bullet 2.
Full code:
class MessageCubit extends Cubit<MessageState> {
final GetMessagesUseCase getMessagesUseCase;
// Added
StreamSubscription<YOUR_MESSAGES_TYPE> sub;
MessageCubit({this.getMessagesUseCase}) : super(MessageInitial());
Future<void> getMessages({String senderId, String recipientId}) async {
emit(MessageLoading());
try {
final messagesStreamData = getMessagesUseCase.call();
// Added
await sub?.cancel();
//This is where I listen to a stream
sub = messagesStreamData.listen((messages) {
emit(MessageLoaded(messages: messages));
});
} on SocketException catch (_) {
emit(MessageFailure());
} catch (_) {
emit(MessageFailure());
}
}
// Added
#override
Future<void> close() async {
await sub?.cancel();
return super.close();
}
}

Related

Use Flutter Bloc one event resulted value in another event under the same bloc

Here i want to reuse ExpenseFetchList event result in ExpenseAdd event. I do not want to call API again. how i can do it under good practice.
ExpenseBloc() : super(ExpenseInitial()) {
on<ExpenseEvent>((ExpenseEvent event, Emitter<ExpenseState> emit) async {
if (event is ExpenseFetchList) {
emit(ExpenseFetching());
List<Expense> e = dummyExpenseList; //await event.useCase.getAll();
// TODO: remove Future.delayed
// ignore: always_specify_types
await Future.delayed(const Duration(seconds: 5), () {
emit(ExpenseFetched(list: e));
});
return;
}
if (event is ExpenseAdd) {
// Here i do not want to call API again
// concatinate above event fetched values with this and return simply.
return;
}
});
}
First of all, you need to add an optional variable for that API response in your parent state. For example its name is ExpenseState
abstract class ExpenseState {
ExpenseState({required this.expenseResponse});
final ExpenseResponseModel? expenseResponse;
}
After that, extend both of your states i.e ExpenseFetchList and ExpenseAdd will have access to the api response variable.
class ExpenseAdd extends ExpenseState { }
class ExpenseFetchList extends ExpenseState { }

flutter How to process bloc events in parallel?

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

Flutter/Riverpod: Call method within StateNotifier from another StateNotifier

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.

flutter_bloc share state for many blocs

let's say I want to check for internet connection every time I call Api, if there's no internet the call with throw exception like NoInternetException and then show a state screen to the user tells him to check their connection.
How can I achieve that without creating a new state for every bloc in flutter_bloc library?
You can do this in the bloc that manages your root pages like authentication_page and homepage.
Create a state for noConnectivity.
NoConnectivity extends AuthenticationState{
final String message;
const NoConnectivity({ this.message });
}
Now create an event for noConnectivity.
NoConnectivityEvent extends AuthenticationEvent{}
Finally, create a StreamSubscription in your AuthenticationBloc to continuously listen to connecitvityState change and if the state is connectivity.none we'll trigger the NoConnecitivity state.
class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationState> {
StreamSubscription subscription;
#override
AuthenticationState get initialState => initialState();
#override
Stream<AuthenticationState> mapEventToState(
AuthenticationEvent event,
) async* {
// ... all other state map
else if(event is NoConnectivityEvent) {
yield* _mapNoConnectivityEventToState();
}
Stream<AuthenticationState> _mapNoConnectivityEventToState() async * {
subscription?.cancel();
//Edit to handle android internet connectivity.
subscription = Connectivity()
.onConnectivityChanged
.listen((ConnectivityResult result) {
if(Platform.isAndroid) {
try {
final lookupResult = InternetAddress.lookup('google.com');
if (lookupResult.isNotEmpty && lookupResult[0].rawAddress.isNotEmpty) {
print('connected');
}
} on SocketException catch (error) {
return add(NoConnectivityState(message: error.message ));
}
} else if(result == ConnectivityResult.none ) {
return add(NoConnectivityState(message: "Noconnection")) ;
}
print("Connected");
});
}
#override
Future<void> close() {
subscription?.cancel();
return super.close();
}
}
This subscription Stream will forever listen to a no connection and push the appropriate page you like to the state.
Required packages
rxdart
connectivity
Hope it helps!
you need base class bloc let's say his name "BaseBloc" and he shall inherit from the "Bloc" class, and implement "mapToStateEvent" method to process "noInternet" exception, and after that call method let's say his name "internalMapToStateEvent" you created, this method it's override method, and inherited all your bloc class from "BaseBloc" and you need same that for pages to draw one widget "noInternetWidget"

Can't subscribe to ValueObservable after all StreamSubscriptions are closed

I have a situation where when all the StreamSubscriptions listening to a ValueObservable are closed, no new StreamSubscriptions that subscribe after that receive any data events.
Is this proper behavior? And if so how can I prevent it?
The best way here is used BloC pattern and in the bloc, you can handle the observables and the streams you need to do something like this:
class LoginBloC implements BaseBloC {
UserRepository _userRepository;
LoginBloC(this._userRepository);
BehaviorSubject<Result<String>> _loginController = new BehaviorSubject();
Stream<Result<String>> get loginResult => _loginController.stream;
void login(String email, String password) async {
updateErrorFromResult(null);
isLoading = true;
final res = await _userRepository.login(email, password);
updateErrorFromResult(res);
sinkAdd(_loginController, res);
isLoading = false;
}
#override
void dispose() {
_loginController.close();
}
}
As you can see in this example you create the streams and later you close them on the dispose method
So in your view, you use the strem doing this:
#override
void initState() {
super.initState();
bloc.loginResult.listen((res) {
if (res is ResultSuccess<String>) {
NavigationUtils.pushReplacement(
context,
HomePage(
));
}
});
}
In this case, you execute navigation when the response of the login is success