Flutter - keep listen a stream on page change - flutter

My scenario is like this:
a BottomNavigationBar where one page contains a list of items ("listPage") and another page is a single item ("itemPage").
both pages can navigate to "itemPage" related to a different items.
the "itemPage" show details of the product and have a "Favourite" toggle button.
in the "listPage" every item shown via ListView.builder, show and can handle the "Favourite" toggle button.
I can't understand how reflect the "Favourite" change in an "itemPage" to others "itemPage" (if opened multiple time for the same item, yes, it's possible) and also to the same item in the "pageList".
I created a NotifierBloc where a BehaviorSubject > Sink is called every time a "Favourite" toggle button change its state (putting the itemId and the boolean value of the Favourite). After a database update an output PublishSubject > Stream is filled with the additional information of the item.
In this way every time a "Favourite" is toggled, all the subscribers receive the info about the flag.
BehaviorSubject<Item> inController = new BehaviorSubject<Item>();
Sink<Item> get putUpdate => inController.sink;
final PublishSubject<Item> outController = PublishSubject<Item>();
Stream<Item> get getUpdates => outController.stream;
NotifierBloc() {
inController.listen(_handleToggle);
}
_handleToggle(Item item) {
...
outController.sink.add(newItemAfterDatabaseCall);
}
In ListPage and ItemPage (both StatefulWidget) I created a StreamSubscription in DidChangeDependencies method which listen to the NotifierBloc Stream.
The StreamSubscription will be cancelled in the Dispose method.
StreamSubscription _subscription;
void didChangeDependencies() {
super.didChangeDependencies();
_subscription = _notifierBloc.getUpdates.listen((item) => {
// Do things with the Item like setState or call bloc methods
...
});
}
void dispose() {
_subscription?.cancel();
super.dispose();
}
Problems are when a new itemPage is opened: the dispose method cancel the subscription in the previous shown page, so new events will not be get listened.
In addition when a page is shown due to a previous page close, the StreamSubscription is renewed and I get updates about the last one Favourite change, but I need a list of Favourite changes, because maybe the user opened several "itemPage"s.
How can I solve?
Maybe the Stream must be passed to the Page(not the PageState)? But how to handle onData function?

You need some sort of state management system. Look into Provider. Here is a simple example of how you could use it. https://github.com/m-Skolnick/provider_example_flutter

Related

Change a dropdown's items when another dropdown's value is chosen in flutter (UI wont update)

I have two drop downs, and I want to do things when they get values selected. One of those is to change the second buttondrop items based on what's selected in the first dropdown.
For example:
Dropdown1 is a list of car manufactuers
Dropdown2 is a list of their models
Dropdown1 selects mercedes
Dropdown2 gets "E Class, S Class" etc
Dropdown1 selects lexus
Dropdown2 gets "ES, LS", etc
(Eventually the second drop down will update a listview as well, but haven't gotten to that yet.)
Data wise, it works, I update the list. The problem is the UI won't update unless I do a hot reload
Currently I am just having the dropdowns fetch their data and using Future builders
Future? data1;
Future? data2;
void initState(){
super.initState();
data1 = _data1AsyncMethod();
data2 = _data2AsyncMethod();
}
_data2AsyncMethod([int? item1_id]) async{
if(item1_id == null){
item2Classes = await DefaultItems().getAllItem2Classes();
listOfItem2ClassNames = DefaultItems().returnListOfItemClassNames(item2Classes);
}
else{
// The methods below calls the DefaultItems methods which have Futures all in them.
// The getAllItems calls a network file with GET methods of future type to get data and decodes them, etc.
// They build a list of the object type, ex List<Item2>
item2Classes = await DefaultItems().getAllItem2Classes(item1_id);
listOfItem2ClassNames = DefaultItems().returnListOfItemClassNames(item2Classes);
}
}
I have this Future Builder nested in some containers and paddings
FutureBuilder{
future: data2,
builder: (context, snapshot){
if(snapshot.connectionState != done...)
// return a circle progress indictator here
else{
return CustomDropDown{
hintText: 'example hint'
dropDownType: 'name'
dropDownList: listOfItem2ClassNames
dropDownCallback: whichDropDown,
}
The onChanged in CustomDropDown passes the dropDownType and the dropDownValue
The callback
whichDropDown(String dropDownType, String dropDownValue){
if(dropDownType == 'item1'){
//so if the first dropdown was used
// some code to get item_1's id and I call the data2 method
_data2AsyncMethod(item1_id);
}
Again the data updates (listOfItem2ClassNames) BUT the UI won't update unless I hot reload. I've even called just setState without any inputs to refresh but doesn't work
So how do I get the UI to update with the data, and is my solution too convoluted in the first place? How should I solve? StreamBuilders? I was having trouble using them.
Thanks
If you do a setState in the whichDropDown function, it will rebuild the UI. Although I'm not exactly sure what you want, your question is really ambiguous.
whichDropDown(String dropDownType, String dropDownValue){
if(dropDownType == 'item1'){
//so if the first dropdown was used
// some code to get item_1's id and I call the data2 method
_data2AsyncMethod(item1_id).then((_) {
setState(() {});
});
}
}
I notice a couple things:
nothing is causing the state to update, which is what causes a rebuild. Usually this is done explicitly with a call to setState()
in whichDropdown(), you call _data2AsyncMethod(item1_id), but that is returning a new Future, not updating data2, which means your FutureBuilder has no reason to update. Future's only go from un-completed to completed once, so once the Future in the FutureBuilder has been completed, there's no reason the widget will update again.
You may want to think about redesigning this widget a bit, perhaps rather than relying on FutureBuilder, instead call setState to react to the completion of the Futures (which can be done repeatedly, as opposed to how FutureBuilder works)

Open dialog box in Flutter Stateless widget using ChangeNotifierProvider

I have a Stateless flutter widget which shows a list of TODOs. The list is shown using a ChangeNotifierProvider (of TodoList object, which contains a list of todos). Now when I load the page, I want to show a dialog box asking user to enter a new TODO if and only if the existing todos is empty. Inside the builder of the ChangeNotifierProvider, i tried below logic
if (todoList.todos.length == 0) {
_showDialog(context);
return Column...;
} else {
return ListBuilder...;
}
But its showing 2 dialog box (probably due to the build method executing twice). I have to pass context to dialog box because I'm updating the todoList inside it, which should trigger a rebuild.
How do I handle this scenario. I've tried using flag (_isDialogOpen) but its not still working?
make the widget Stateful in order to use it's lifecycle methods, you can use then initState() for showing the dialog when the page widgets are inserted in the widget tree, but you will need to use an addPostFrameCallback() to schedule showing it 1 frame after the initState's code gets executed:
First, import:
import package:flutter/scheduler.dart.
Then use this:
#override
void initState() {
// ...
SchedulerBinding.addPostFrameCallback((_) => _showDialog(context),);
}

How to attend best practice for not using UI code in the Controller with GetX flutter when I need to show a Dialog if my task complete.?

For a simple Email login with OTP code I have a structure as follows.
View
await _signUpCntrl.signUp(email, password);
Controller
_showOtpDialog(email);
_showOtpDialog func
return Get.dialog(
AlertDialog(
So the thing is _showOtpDialog function is inside a controller file. ie. /Controllers/controller_file.dart
I want do something like a blocListener, call the _showOtpDialog from a screen(view) file on signup success. (also relocate the _showOtpDialog to a view file)
Using GetX I have to use one of the builders either obs or getbuilder. Which is I think not a good approach to show a dialog box.
On internet it says Workers are the alternative to BlocListener. However Workers function resides on Controller file and with that the dialog is still being called on the controller file.
As OTP dialog will have its own state and a controller I wanted to put it inside a /view/viewfile.dart
How do I obtain this?
I tried using StateMixin but when I call Get.dialog() it throw an error.
visitChildElements() called during build
Unlike BLoC there's no BlocListener or BlocConsumer in GetX.
Instead GetX has RxWorkers. You can store your response object in a Rx variable:
class SomeController extends GetxController{
final response= Rxn<SomeResponse>();
Future<void> someMethod()async{
response.value = await someApiCall();
}
}
And then right before the return of your widget's build method:
class SomeWidget extends StatelessWidget{
final controller = Get.put(SomeController());
#override
Widget build(BuildContext context){
ever(controller.response, (SomeResponse res){
if(res.success){
return Get.dialog(SuccessDialog()); //Or snackbar, or navigate to another page
}
....
});
return UI();
}
First thing, you will need to enhance the quality of your question by making things more clearly. Add the code block and the number list, highlight those and making emphasize texts are bold. Use the code block instead of quote.
Seconds things, Depends on the state management you are using, we will have different approaches:
Bloc (As you already added to the question tag). By using this state management, you controller ( business logic handler) will act like the view model in the MVVM architecture. In terms of that, You will need to emit a state (e.g: Sent success event). Afterward, the UI will listen to the changes and update it value according to the event you have emitted. See this Bloc example
GetX (As your code and question pointed out): GetX will acts a little bit different. you have multiple ways to implement this:
Using callbacks (passed at the start when calling the send otp function)
Declare a general dialog for your application ( this is the most used when it comes to realization) and calling show Dialog from Bloc
Using Rx. You will define a Reactive Variable for e.g final success = RxBool(true). Then the view will listen and update whenever the success changes.
controller.dart
class MyController extends GetxController {
final success = RxBool(false);
void sendOtp() async {
final result = await repository.sendOTP();
success.update((val) => {true});
}
}
view.dart
class MyUI extends GetView<MyController> {
#override
Widget build(BuildContext context) {
ever(controller.success, (bool success) {
// This will update things whenever success is updated
if (success) {
Get.dialog(AlertDialog());
}
});
return Container();
}
}

flutter rxdart Observable - how to unsubscribe

Here is my use case:
I have a following stream set:
PublishSubject<RewardedVideoAdEvent> _outVideoAdController =
PublishSubject<RewardedVideoAdEvent>();
StreamSink<RewardedVideoAdEvent> get _videoAdEvents => _outVideoAdController.sink;
Observable<RewardedVideoAdEvent> get outVideoAdEvents => _outVideoAdController.stream;
Now, I want to listen to outVideoAdEvents, hence I add this to my StatefullWidget initState method:
...
if (mounted) {
final AdMobBloc adMob =
BlocProvider.of<AppProvider>(context).application.adMobBloc;
adMob.outVideoAdEvents.listen((RewardedVideoAdEvent event) {
if (event == RewardedVideoAdEvent.rewarded){
// do something meaningfull
}
});
...
So far, so good.
The issue I have got - when if I open another widget and then come back to this one, initState executes again and hence, I have added another listener to the same stream. Then, the next time I issue an event into _outVideoAdController.sink, the callback will be executed twice.
Unfortunately, unlike initState, dispose does not execute each time I load another page, so I cannot figure out how to handle the above case.
Please note, the app uses rxdart: ^0.20.0
Any hints will be greatly appreciated!
you can unsubscribe the observable by this method:
_outVideoAdController?.close()
In case, someone else came across the same issue, these are the steps:
Add a private variable inside the widget from type StreamSubscription<T>, where T is your event type. In my case it is RewardedVideoAdEvent, hence I added StreamSubscription<RewardedVideoAdEvent> _videoAdSubscription;.
Then, when subscribing to the stream, the listen will return the value from this type, so just take it: _videoAdSubscription = adMob.outVideoAdEvents.listen((RewardedVideoAdEvent event) {});
Finally, when you want to unsubscribe, just call _videoAdSubscription?.cancel();
That's all.

Get access to the last yielded state within the Flutter BloC

I have two screens: first one is list of items and second is the form page for editing or creating items. Each page is managed via dedicated BloC - ItemListBloc and FormBloc respectively, using flutter_bloc library. Whenever the submit is called on the Form I want to react to this in ItemListBloc and modify or insert item in the list to avoid refetching whole thing from the backend.
This is how I try to achieve this:
ItemListBloc({ #required this.itemFormBloc, ... }) {
itemFormSub = itemFormBloc.state.listen((state) {
if (state is FormSubmitted) {
dispatch(ModifyOrInsert(isEdit: state.isEdit, item: state.isItem));
}
});
}
#override
Stream<ItemListState> mapEventToState(
ItemListEvent event,
) async* {
if (event is ModifyOrInsert) {
yield* _modifyOrInsert(isEdit: event.isEdit, item: event.item);
}
}
Stream<ItemListState> _modifyOrInsert({bool isEdit, Item item}) async* {
// How to get access to the list of items here?
}
Unlike Redux-y global state I have number of states which I can manipulate and declare one of them current. The last state of ItemListBloc is typically ItemsLoaded which holds the whole items list. I need to get access to this state, take items, update item in list if it was an edit, prepend item to the list if it was create and silently yield updated ItemsLoaded state afterwards.