I am losing stream data when navigating to another screen - flutter

I am new to Dart/Flutter and after "attending" a Udemy course,
everything has been going well.
Until now ;-)
As in the sample application in the Udemy course i am using the BLOC pattern.
Like this:
class App extends StatelessWidget {
Widget build(context) {
return AppBlocProvider(
child: MaterialApp(
(See "AppBlocProvider" which I later on use to get the "AppBloc")
The App as well as all the screens are StatelessWidget's.
The AppBlocProvider extends the InheritedWidget.
Like this:
class AppBlocProvider extends InheritedWidget {
final AppBloc bloc;
AppBlocProvider({Key key, Widget child})
: bloc = AppBloc(),
super(key: key, child: child);
bool updateShouldNotify(_) => true;
static AppBloc of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(AppBlocProvider) as AppBlocProvider).bloc;
}
}
The AppBlocProvider provides an "AppBloc" containing two further bloc's to separate the different data a bit.
Like this:
class AppBloc {
//Variables holding the continuous state
Locale appLocale;
final UserBloc userBloc;
final GroupsBloc groupsBlock;
In my application I have a "GroupSearchScreen" with just one entry field, where you can enter a fragment of a group name. When clicking a button, a REST API call is done and list of group names is returned.
As in the sample application, I put the data in a stream too.
In the sample application the data fetching and putting it in the stream is done in the bloc itself.
On the next line, the screen that uses the data, is created.
Like this:
//Collecting data and putting it in the stream
storiesBloc.fetchTopIds();
//Creating a screen ths shows a list
return NewsList();
In my case however, there are two major differences:
After collecting the data in the GroupSearchScreen, I call/create the GroupsListScreen, where the list of groups shall be shown, using regular routing.
Like this:
//Add data to stream ("changeGroupList" privides the add function of the stream!)
appBloc.groupsBlock.changeGroupList(groups);
//Call/create screen to show list of groups
Navigator.pushNamed(context, '/groups_list');
In the GroupsListScreen, that is created, I fetch the bloc.
Like this:
Widget build(context) {
final AppBloc appBloc = AppBlocProvider.of(context);
These are the routes:
Route routes(RouteSettings settings) {
switch (settings.name) {
case '/':
return createLoginScreen();
case '/search_group':
return createSearchGroupScreen();
case '/groups_list':
return createGroupsListScreen();
default:
return null;
}
}//routes
And "/groups_list" points to this function:
Route createSearchGroupScreen() {
return MaterialPageRoute(
builder: (context) {
//Do we need a DashboardScreen BLOC?
//final storiesBloc = StoriesProvider.of(context);
//storiesBloc.fetchTopIds();
return GroupSearchScreen();
}
);
}
As you can see, the "AppBlocProvider" is only used once.
(I ran into that problem too ;-)
Now to the problem:
When the GroupsListScreen starts rendering the list, the stream/snapshot has no data!
(See "if (!snapshot.hasData)" )
Widget buildList(AppBloc appBloc) {
return StreamBuilder(
stream: appBloc.groupsBlock.groups,
builder: (context, AsyncSnapshot<List<Map<String, dynamic>>>snapshot) {
if (!snapshot.hasData) {
In order to test if all data in the bloc gets lost, I tried not to put the data in the stream directly, but in a member variable (in the bloc!).
In GroupSearchScreen I put the json data in a member variable in the bloc.
Now, just before the GroupsListScreen starts rendering the list, I take the data (json) out of the bloc, put it in the stream, which still resides in the bloc, and everything works fine!
The snapshot has data...
Like this (in the GroupsListScreen):
//Add data to Stream
appBloc.groupsBlock.changeGroupList(appBloc.groupsBlock.groupSearchResult);
Why on earth is the stream "losing" its data on the way from "GroupSearchScreen" to "GroupsListScreen" when the ordinary member variable is not? Both reside in the same bloc!
At the start of the build method of the GroupsListScreen, I have a print statement.
Hence I can see that GroupsListScreen is built twice.
That should be normal, but could that be the reason for not finding data in the stream?
Is the Stream listened on twice?
Widget buildList(AppBloc appBloc) {
return StreamBuilder(
stream: appBloc.groupsBlock.groups,
I tried to explain my problem this way, not providing tons of code.
But I don't know if it's enough to give a hint where I can continue to search...
Update 16.04.2019 - SOLUTION:
I built up my first app using another app seen in a Udemy course...
The most important difference between "his" app and mine is that he creates the Widget that listens to the stream and then adds data to the stream.
I add data to the stream and then navigate to the Widget that shows the data.
Unfortunately I used an RX-Dart "PublishSubject." If you listen to that one you will get all the data put in the stream starting at that time you started listening!
An RX-Dart "BehaviorSubject" however, will also give you the last data, just before you started listening.
And that's the behavior I needed here:
Put data on stream
Create Widget and start listening
I can encourage all Flutter newbies to read both of these very good tutorials:
https://medium.com/flutter-community/reactive-programming-streams-bloc-6f0d2bd2d248
https://www.didierboelens.com/2018/12/reactive-programming---streams---bloc---practical-use-cases/
In the first one, both of the streams mentioned, are explained very well.

Related

Where to put bloc providers in flutter

I have tried to use bloc in flutter for a period, but the question of where to put bloc provider confuse me.
At the beginning of learning flutter, I put nearly all the providers in the main() function by using MultipleBlocProvider.
But due to some of my blocs subscribe to streams such as firebase snapshots, I think it might cost extra resources (I am not sure but at least some memory was occupied I guess) if I do not close those subscriptions by closing the bloc.
However, if the provider is in the main(), change page or pop up would not close these blocs and the subscriptions inside.
In this scenario, I try to put some bloc providers into specific pages, so the bloc can be close and recreate when I goes in and out of this page. But there are some other questions occurs.
For example, if I have a bloc called ProductDetailsBloc which is used to control the widget of product details in product details page, its events contains an event called GetProductBySku which is used to get product from firebase and set the product inside the state(ProductDetailsState).
I want to put this bloc and its provider inside product details page and put an event trigger inside product list widget (located in product list page) and its onTap() function, where users click on the product list item (this is a thumbnail product item which is from another resources and only contains very basic info such as sku, title ) inside product list page and then navigator to product details page to view the full information of this product.
But as I mentioned before, If I put the bloc provider of ProductDetailsBloc inside product details page, I can not use GetProductBySku before entering product details page
So, I personally have two ideas for this questions,
the first one is passing the product sku through arguments to product details page and then call the GetProductBySku after the bloc has been created.
the second one is the put the ProductDetailsBloc inside the main(), so I can skip this questions, and directly use the GetProductBySku in product list widget, but this turns the problems in to the very front.
I do not know which one is better, so I would be very appreciative if someone can give some suggestions.
And back to the main questions, what is the best practice of putting bloc providers and what are the concerns if I put providers inside main().
Thanks for every one that read to here because this is a bit long and this is my first time of asking on attack overflow :3
You can find more discussion regarding this in the discord server of Bloc. More info: https://discord.com/channels/649708778631200778/649708993773699083/860604230930006016
I will copy paste the message and code here. we have been using bloc
in our app for over a year and it seems we have got an issue.
Scenario: List of Posts needs to be shown in home feed. Let's say each
post is encapsulated in PostBloc (operations on post to be performed
here). I have been using moor to reactively find if anything changes
in the data (like number of likes of post which might have trigged
from post detail page where post detail was fetched again). I want to
know if it is a wise decision to create BlocFactory which will be
providing Bloc object (here different PostBloc object) based on
Post_Id.
I have written a sample BlocFactoryProvider, please let me know your
thoughts
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';
typedef CreateIfAbsent<T> = T Function();
//create bloc of <T> type
//author: https://github.com/imrhk
class BlocFactory<T extends Bloc<dynamic, dynamic>> {
Map<String, T> _cache = <String, T>{};
T createBloc({#required String id, #required CreateIfAbsent<T> createIfAbsent}) {
assert(id != null && createIfAbsent != null);
if (_cache.containsKey(id)) {
return _cache[id];
}
final value = createIfAbsent.call();
_cache[id] = value;
return value;
}
void dispose() {
_cache.values.forEach((bloc) => bloc?.close());
_cache.clear();
}
}
class BlocFactoryProvider<T extends Bloc<dynamic, dynamic>, V>
extends SingleChildStatelessWidget {
final BlocFactory<T> _blocFactory;
final Widget child;
BlocFactoryProvider({BlocFactory<T> blocFactory, this.child})
: _blocFactory = blocFactory ?? BlocFactory<T>();
#override
Widget buildWithChild(BuildContext context, Widget child) {
return InheritedProvider<BlocFactory<T>>(
create: (_) => _blocFactory,
dispose: (_, __) => _blocFactory.dispose(),
child: child,
lazy: false,
);
}
static BlocFactory<T> of<T extends Bloc<dynamic, dynamic>>(BuildContext context) {
try {
return Provider.of<BlocFactory<T>>(context, listen: false);
} on ProviderNotFoundException catch (e) {
if (e.valueType != T) rethrow;
throw FlutterError(
"""
BlocProvider.of() called with a context that does not contain a Bloc of type $T.
No ancestor could be found starting from the context that was passed to BlocProvider.of<$T>().
This can happen if the context you used comes from a widget above the BlocProvider.
The context used was: $context
""",
);
}
}
Felix's replay was
It’s hard to say without more context but I’d recommend moving the
caching to the repository layer and instead have a single PostBloc
with PostChanged event that pulls the latest details (using cache).
So he did not completely discarded the idea.
declaration: I am the author of the below code shared over discord in March'21.

Flutter BlocProvider consumption

I'm implementing a BLoC pattern for state management in my Fluter application. As I'm new in Flutter and BLoC particularly I'm evolving its usage gradually.
For new I use BLoC to communicate between two pages. One page sends an asset to the BLoC and navigates to details page. The details page uses StreamBuilder to read from the BLoC and build page with according data:
AppWidget:
Widget build(BuildContext context) => MultiProvider(
providers: [
BlocProvider(create: (context) => AssetBloc())
...
Requesting page
_onAssetMenuAction(BuildContext context, AssetMenu value, Asset asset) {
switch (value) {
case AssetMenu.validate:
var bloc = BlocProvider.of<AssetBloc>(context);
bloc.validate(asset);
Navigator.push(context,
MaterialPageRoute(builder: (context) => ValidateAssetPage()));
break;
}
Validation page
Widget build(BuildContext context) {
var bloc = BlocProvider.of<AssetBloc>(context);
Logger.root.info("Building validation page");
return StreamBuilder<AssetValidation>(
stream: bloc.outValidation,
builder: (context, snapshot) => snapshot.hasData
? QrImage.withQr(qr: snapshot.data!.qr)
: Text("No QR"));
}
BLoC
class AssetBloc extends BlocBase {
//
// Stream to handle the validation request outcome
//
StreamController<AssetValidation> _validationController =
StreamController<AssetValidation>.broadcast();
StreamSink<AssetValidation> get _inValidation => _validationController.sink;
Stream<AssetValidation> get outValidation => _validationController.stream;
//
// Stream to handle the validation request
//
StreamController<Asset> _validateController = StreamController<Asset>();
void Function(Asset) get validate => _validateController.sink.add;
//
// Constructor
//
AssetBloc([state]) : super(state) {
_validateController.stream.listen(_handleLogic);
}
void _handleLogic(Asset asset) {
_inValidation.add(AssetValidation.create(asset));
Logger.root.finest("AssetValidation instance is sent to stream");
}
void dispose() {
_validateController.close();
_validationController.close();
}
}
The problem I have is I'm getting "No QR". According to logs I see following sequence of actions:
new AssetValidation.create(): Validating asset Instance of 'Asset'
AssetBloc._handleLogic(): AssetValidation instance is sent to stream
ValidateAssetPage.build(): Building validation page
So at the moment of validation page building the validation result data should be in the stream but it seems they are not.
Unit tests of AssetBloc work as expected. So I suspect it should be related to StreamBuilder in validation page.
The StreamBuilder just shows you the last value of the stream whether the StreamBuilder was present on the current deployed widget when the stream was updated. So, if you add a new value to the stream, but the StreamBuilder is not on the current deployed widget, and, after that, you deploy the widget with the StreamBuilder, it's very likely that it won't show the updated data (in fact it shows empty data). I know, it's weird, i have the same problem when i like to use streams in that way. So, instead, i recommend you to use ValueListenable on the bloc and ValueListenableBuilder on the widget. It's very useful for that cases.
Another thing to point out is that if you're going to use just streams for the state management, it's better to use another state manager type such as provider or singleton. The reason is that, the right way to use bloc (the way you take advantage of the power of bloc) is using just the method add() for the events and logic, and using the established bloc State classes to show and update the data with the BlocBuilder on the widget.

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

Nested Flutter Futurebuilder is not updating the view

I have a Stateful widget that I am displaying inside a stateless widget. I have reused this Widget in several parts of my application, where it worked.
The widget looks a somethin like this
class InnerWidget extends StatefulWidget {
// private keys for objects in the remote dataahase
List<int> privateKeys;
const InnerWidgt({
Key key,
this.privateKeys,
}) : super(key: key);
#override
_InnerWidgetState createState() => _InnerWidgetState();
}
class _InnerWidgetState extends State<InnerWidget> {
// data retrieved from the database
Future<List<SomeClass>> data;
#override
void initState() {
this.data = this.fetchData(widget.privateKeys);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<SomeClass>> (
future: this.data,
builder: (BuildContext contest, AsyncSnapshot<List<SomeClass>> snapshot) {
if(snapshot.hasData){
// return a widget (a list of names in this case)
}
return Text('Loading...');
}
);
}
Future<List<SomeClass>> fetchData(List<int> pKeys){
// fetches the JSON objects from the server and returns them as instances
}
}
The widget is created and given a list of keys, it makes a call to the remote API to get some objects and displays them in a list. I thought it might be nice that this widget is kind of handling itself, but maybe it's bad design?
What happens, is that the fetchData() method is never called and the "Loading..." is displayed forever. However snapshot.hasData does return true, so I'm thinking this is some kind of repainting issue.
The view I am talking about, where this is used is a detail view, which you navigate to from a list (ListView). I am using this exact same InnerWidget in the the elements inside the ListView, where it works perfectly fine. But when I click on the item to navigate to the detail page, the same InnerWidget does not work. (I know fetching the same data twice is not good, I have that in the back of my head to change it, and there's already a Cache underneath). Maybe this has something to do with the preserved state in the ElementTree? That because I am using the same InnerWidgit, it's preserving its state and not rerendering?
I hope I phrased my question well enough, I did not want to throw hundreds of lines of code at you, that's why created this minimal example of the widget at least. I can add the list and navigation part too if needed, but maybe this is something, that somebody that has worked more with Flutter, knows right away, I'm only 3 weeks in.

BLoCs and multiple streams - Is there a better solution?

Currently I'm working with BLoCs in Flutter and I've got a question about multiple streams within a Bloc.
For example when a screen has got multiple widgets which should depend on the Bloc. I could wrap the whole screen in the StreamBuilder, but then every time all widgets would be rebuilt.
The example bloc:
class TestBloc {
final StreamController _dataController = StreamController<String>();
final StreamController _appBarTitleController = StreamController<String>();
TestBloc();
Stream<String> get appBarTitle => _appBarTitleController.stream;
Stream<DataState> get data => _dataController.stream;
void fetchData(String path) async {
_dataController.sink.add(PokemonDataLoading());
Data data = await _getData();
_dataController.sink.add(Loaded(data));
_appBarTitleController.sink.add(data.name);
}
Future<Data> _getData(String path) async {
return await _dataRepository.fetchData(path);
}
void dispose() {
_dataController.close();
_appBarTitleController.close();
}
}
On the example build method you can see two different StreamBuilders, one for the app bar title and one for the content. Of course I could wrap them in this example into one StreamBuilder, but sometimes this isn't easily possible. They may depend on other data or user interactions.
#override
Widget build(BuildContext context) {
_testBloc.fetchData();
return ScaffoldWithSafeArea(
title: StreamBuilder(
stream: _testBloc.appBarTitle,
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data);
}
return Text("Test");
},
),
child: StreamBuilder<DataState>(
stream: _testBloc.data,
builder: (context, AsyncSnapshot<DataState> snapshot) {
DataState state = snapshot.data;
if (state is DataInitial) {
return _buildLoading();
} else if (state is DataLoaded) {
return _buildContent(state.data);
}
return _buildLoading();
},
),
);
}
Is there maybe a better solution for multiple Streams on one screen? I use a lot of boilerplate code here and would like to avoid this.
In order to manage multiple streams in one screen, the best solution is to have multiple widgets that listen the corresponding stream.
This way, you increase the performance of your app by optimizing the total number of builds of your widgets.
By doing this, you can create widgets that listen an output (stream) of your BLoC and reuse them in different parts of your app, but in order to make the widget reusable you need to inject the BLoC into the widget.
If you see the BLoC UI design guidelines
Each "complex enough" component has a corresponding BLoC
This way your screen will now be composed of different components, and this component is a widget that listens to an output (stream) of your BLoC.
So you are doing things right.
If you want to reduce the repetitive code a bit in your widgets, you can:
Create your own widget that listens to the stream and directly returns the output of the BLoC (in your case you call state), this way you don't need to use snapshot.data like in the StreamBuilder. The example of this widget is the BlocBuilder of flutter bloc library.
Use the flutter bloc library that has widgets that reduce the boilerplate code when use BLoC pattern, but if you use this library, you now need to create your BLoCs using bloc library, but if you do this, now you reduce the boilerplate code of creating StreamControllers in your BLoC, and other interesting features, so you should take a look the power of bloc and flutter bloc libraries.