I have a Cubit that retrieves json data from an API. It processes the data and based on the processing, will need to change the state of multiple widgets.
Essentially, using some if statements, the state changes will need to be emitted if the data matches certain criterion.
This code sample shows the idea, but I'm not sure how to actually fulfill the need within the if statements.
import 'dart:convert';
import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
class ProcessingCubit extends Cubit<String> {
ProcessingCubit() : super("");
void getDataFromAPI() async {
Response response;
var dio = Dio();
response = await dio.get(
'http://our.internalserver.com:8080/api/getdata.php',
queryParameters: {});
var parsedjsonresponse = json.decode(response.data.toString());
//the json returned is an array of objects. For this code example,
//we're only going through slot 0 of the array of objects
if (!parsedjsonresponse['ourdata'].isEmpty) {
print(parsedjsonresponse['ourdata']);
}
if (!parsedjsonresponse['ourdata'][0]['code'] == "001") {
//emit state for this code, so that the necessary widget
//will show something
}
if (!parsedjsonresponse['ourdata'][0]['code'] == "002") {
//emit state for this code, so that the necessary widget will
//show something (different widget than the "if" block above
}
if (!parsedjsonresponse['ourdata'][0]['alert'] == "1") {
//emit state for this alert so that the alert widget
//will show something
}
}
}
Sometimes none of the if statements will need to change state, sometimes all may need to, and sometimes only some.
You can emit states using:
emit(CubitState);
Since you declared your Cubit State to be a String it would be:
emit("apiResponseAsString");
You can emit as much states as you want. So for each of your ifs you can emit the according string.
The official documentation for the bloc library gives you great examples for cubits.
Related
Is it ok to return a value from a Cubit state function or is it better to emit a state and use BlocListener?
Future<Game?> addGame(List<String> players, int numOfRounds) async {
try {
Game game = await repository.addGame(DateTime.now(), players, numOfRounds);
return game;
} on Exception {
emit(GamesError(message: "Could not fetch the list, please try again later!"));
}
}
The widget that calls this function adds a game and then redirects to a new page and passes the game object to it.
This works but it doesn't feel like it is the right approach. Is it ok to do this or should I be emitting a new state and using the BlocListener to redirect to the new page?
Of course, it's not.
Bloc/Cubit is the single source of truth for the widget. All data that comes to the widget should be passed via state, one source. If you return values from Cubit methods, you are breaking the whole concept of the Bloc pattern.
Bloc data flow
It is ok, but not preferred.
Presently the function addGame returns a future, so you would have to use FutureBuilder to display it's value.
Instead emit state having containing the value,Now you can use BlocListener and BlocBuilder to display the value of game produced in the function addGame. So now the purpose of using bloc makes sense.
Use code like:
Future<Game?> addGame(List<String> players, int numOfRounds) async {
try {
Game game = await repository.addGame(DateTime.now(), players, numOfRounds);
emit(GameLoaded(game: game); // 👈 Use it this way
} on Exception {
emit(GamesError(message: "Could not fetch the list, please try again later!"));
}
}
What I wanted to achieve, was to retrieve a document from firestore, and provide it down the widget tree, and be able to make changes on the data of the document itself.
show something like "No data available/selected" when there's nothing to display,
show loading-screen/widget if the document is loading,
show data in the UI when document is ready
be able to make changes to the data of the document itself (in firestore) AND reflect those changes in the UI, WITHOUT reading the document again from firestore
be able to reload the document/load another document from firestore, show a loading screen while waiting for document, then show data again
The whole purpose of this, is to avoid too many firestore read operations. I update the document data on the server (firestore), then make the same updates in the frontend (an ugly alternative would be to retrieve the document from firestore each time I make a change).
If the changes are too complex though, or if it is an operation that is rarely executed, it might just be a better idea to read the whole document again.
"Answer your own question – share your knowledge, Q&A-style" -> see my solution to this problem in my answer below
The above described functionalities are NOT POSSIBLE with:
FutureProvider: you can use this to retrieve a document from firestore and show a loading-screen while waiting, but can't make changes to it afterwards, that will also show in the UI
FutureBuilder: same as above, even worse, this can't provide data down the widget-tree
It was however possible with ChangeNotifierProvider (& ChangeNotifier), and this is how I did it:
enum DocumentDataStatus { noData, loadingData, dataAvailable }
...
class QuestionModel extends ChangeNotifier {
DocumentSnapshot _questionDoc; //the document object, as retrieved from firestore
dynamic _data; //the data of the document, think of it as Map<String, dynamic>
DocumentDataStatus _documentDataStatus;
DocumentDataStatus get status => _documentDataStatus;
//constructors
QuestionModel.example1NoInitialData() {
_documentDataStatus = DocumentDataStatus.noData; //no question selected at first
}
QuestionModel.example2WithInitialData() {
_documentDataStatus = DocumentDataStatus.loadingData; //waiting for default document or something...
//can't use async/await syntax in a dart constructor
FirebaseFirestore.instance.collection('quiz').doc('defaultQuestion').get().then((doc) {
_questionDoc = doc;
_data = _questionDoc.data();
_documentDataStatus = DocumentDataStatus.dataAvailable;
notifyListeners(); //now UI will show document data
});
}
//all kinds of getters for specific data of the firestore document
dynamic get questionText => _data['question'];
dynamic get answerText => _data['answer'];
// dynamic get ...
///if operation too complex to update in frontend (or if lazy dev), just reload question-document
Future<void> loadQuestionFromFirestore(String questionID) async {
_data = null;
_documentDataStatus = DocumentDataStatus.loadingData;
notifyListeners(); //now UI will show loading-screen while waiting for document
_questionDoc = await FirebaseFirestore.instance.collection('quiz').doc(questionID).get();
_data = _questionDoc.data();
_documentDataStatus = DocumentDataStatus.dataAvailable;
notifyListeners(); //now UI will show document data
}
///instantly update data in the UI
void updateQuestionTextInUI(String newText) {
_data['question'] = newText; //to show new data in UI (this line does nothing on the backend)
notifyListeners(); //UI will instantly update with new data
}
}
...
Widget build(BuildContext context) {
QuestionModel questionModel = context.watch<QuestionModel>();
return [
Text('No question to show'),
Loading(),
Text(questionModel.questionText),
][questionModel.status.index]; //display ONE widget of the list depending on the value of the enum
}
The code in my example is simplified for explanation. I have implemented it in a larger scale, and everything works just as expected.
Problem Summary:
I'm trying to fetch a list from StateA of BlocA when I create a new bloc.
Simplified Background:
I have an overarching bloc (BlocA), that is always active in the context of this problem, and 2 screens with a corresponding bloc each (BlocB & BlocC) that gets created when routing to its associated screen and closes when routing away from its associated screen. Every time a new bloc is created it needs to fetch its data from the state of BlocA. The user might move back and forth between screens.
What I tried:
I created stream controllers in BlocA that streams relevant data to each of the blocs via a getter. At first, I tried a normal (single listner) stream which worked fine initially. However, when routing away and then back to the screen it throws an error when resubscribing to the same stream using the getter. I then instantiated the stream controller as a broadcast stream StreamController.broadcast(). The problem is then that, when subscribing to the stream, no data is passed on subscription to the stream like with a normal stream and when I try to implement an onListen callback in the broadcast constructor (to add an event to the sink) it gives me an error The instance member '_aStream' can't be accessed in an initializer. A similar error appears for state. See below:
... _aStream = StreamController.broadcast(onListen: () {return _aStream.add(state.TypeX)})
Simplified Example Code:
class BlocA extends Bloc<BlocAEvent, BlocAState> {
BlocA() : super(BlocAState()) {
on<EventA>(_onevent);
}
final StreamController<TypeX> _aStream = StreamController.broadcast();
Stream<TypeX> get aStream => _aStream.stream;
final StreamController<TypeY> _bStream = StreamController.broadcast();
Stream<TypeY> get bStream => _bStream.stream;
...
// sink.add()'s are implemented in events
}
class BlocB extends Bloc<BlocBEvent, BlocBState> {
BlocB({required this.blocA}) : super(BlocBState()) {
on<EventB>(_onEventB);
blocASubscription = blocA.aStream.listen((stream) {
if (stream != state.fieldX) {
add(EventB(fieldX: stream));
}
});
}
final BlocA blocA
late final StreamSubscription blocASubscription;
FutureOr<void> _onEventB(EventB event, Emitter<BlocBState> emit) {
emit(state.copyWith(fieldX: event.fieldX));
}
}
class BlocC extends Bloc<BlocCEvent, BlocCState> {
// similar to BlocB
}
You do not need a stream, because bloc underhood is on streams yet. You can sent everything what you want through events and states. Check the library of Angelov https://bloclibrary.dev/#/
I ended up staying with the stream controllers, as used in the example code, but created a new event for BlocA where it is triggered when the user changes between screens and sinks the appropriate state data into the stream. The event carried an index field to indicate the screen that was routed to. The event's index corresponds with the navBar index.
The event handling implementation looked like this:
FutureOr<void> _onScreenChanged(
ScreenChanged event,
Emitter<BlocAState> emit,
) async {
switch (event.index) {
case 0:
_aStream.sink.add(state.fieldX);
break;
case 1:
_bStream.sink.add(state.fieldY);
break;
default:
}
}
I have this ViewModel and a Riverpod provider for it:
final signInViewModelProvider = Provider.autoDispose<SignInViewModel>((ref) {
final vm = SignInViewModel();
ref.onDispose(() {
vm.cleanUp();
});
return vm;
});
class SignInViewModel extends VpViewModelNew {
FormGroup get form => _form;
String get emailKey => _emailKey;
String get passwordKey => _passwordKey;
final String _emailKey = UserSignInFieldKeys.email;
final String _passwordKey = UserSignInFieldKeys.password;
final FormGroup _form = FormGroup({
UserSignInFieldKeys.email:
FormControl<String>(validators: [Validators.required]),
UserSignInFieldKeys.password:
FormControl<String>(validators: [Validators.required])
});
void cleanUp() {
print('cleaning up');
}
void onSubmitPressed(BuildContext context) {
// _saveRegistrationLocallyUseCase.invoke(
// form.control(_self.emailKey).value as String ?? '',
// form.control(_self.passwordKey).value as String ?? '');
}
}
abstract class VpViewModelNew {
VpViewModelNew() {
if (onCreate != null) {
onCreate();
print('creating');
}
}
void onCreate() {}
}
When I navigate to the page that has the signInViewModelProvider, it prints to the console:
flutter: signInPage building
flutter: creating
flutter: cleaning up
Then popping the page from the stack with Navigator.pop() prints nothing.
Then navigating to the page again prints the same 3 lines in the same order.
I expected onDispose to be called after Navigator.pop(), and not when navigating to the page that reads the provider. Why is onDispose being called directly after creation, and not when using Navigator.pop() (when I expected the provider to be disposed of since no other views reference it)?
Edit: I access the provider with final viewModel = context.read<SignInViewModel>(signInViewModelProvider);
I don't need to listen since I don't need to rebuild the page on
change. Is consumer less performant for this?
No, the performance is meaningless, even if it's listening it's not really affecting the performance because as a Provider there is no way to notify (which is not the case with a state notifier or change notifier)
Also if you don't care to listen after the value has been read The auto dispose understand no one is watching it and it disposes, it's better to use context.read when using tap or gestures that modify something
(I realize this is late to the party but maybe it'll help somebody)
The Riverpod docs come out pretty strongly against using read for the reason you said, i.e. performance/rebuilding concerns.
Basically you should always use watch except:
If you want your custom callback function called when it updates (use listen)
If the actual reading is happening asynchronously or in response to user action (like in an onPressed): this is the only time to use read.
If you're having issues with your widgets rebuilding too often, Riverpod has some ways to deal with that that don't involve using read.
I'm using StreamControllers with Flutter. I have a model with some default values. From the widgets where I'm listening to the stream I want to supply some of those default values. I can see I can set an initial value on the StreamBuilder, but I want to use data from the model inside the bloc as initial data. So as soon as someone is using the snapshot data they get the default values. I've seen RxDart has a seed value, just wondering if this is possible without replacing with RxDart?
What you are looking for is StreamController#add method,
Sends a data event.
Listeners receive this event in a later microtask.
Note that a synchronous controller (created by passing true to the
sync parameter of the StreamController constructor) delivers events
immediately. Since this behavior violates the contract mentioned here,
synchronous controllers should only be used as described in the
documentation to ensure that the delivered events always appear as if
they were delivered in a separate microtask.
happy fluttering
The default value for a stream can be specified when the class is initialized after adding a listener for that stream.
import 'dart:async';
enum CounterEvent { increase }
class CounterBloc {
int value = 0;
final _stateCntrl = StreamController<int>();
final _eventCntrl = StreamController<CounterEvent>();
Stream<int> get state => _stateCntrl.stream;
Sink<CounterEvent> get event => _eventCntrl.sink;
CounterBloc() {
_eventCntrl.stream.listen((event) {
_handleEvent(event);
});
_stateCntrl.add(value); // <--- add default value
}
void dispose() {
_stateCntrl.close();
_eventCntrl.close();
}
_handleEvent(CounterEvent event) async {
if (event == CounterEvent.increase) {
value++;
}
_stateCntrl.add(value);
}
}