Flutter state and streams - flutter

This kind of a code structure question. I have two pages in my app page1 and page2. Page2 y irrelevant except that you can navigate from page1 to page2 and back to page1.
In page1 I use this plugin . It shows a dialog and you provide a call back with the selected language. I use a Stream because I have to load a predefined language from shared preferences which is async. My intention is to pass _lang to page2. The code as I have it is working as expected. But if you navigate back to page1 from page2 you get the default language because I didn't call setState. If I put a setState inside the callback the changes don't persist because the stream gets rebuild every time. And I cannot take the stream out if I want the default to show on start.
StreamBuilder(
stream: langStream.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
_lenguaje = snapshot.data;
return ListTile(
leading: Icon(Icons.language),
title: Text('${snapshot.data.name}'),
trailing: Text('Your Lang'),
onTap: () {
showLangPicker(
context,
(Language lang) {
_lang = lang;
langStream.update(lang);
},
);
},
);
} else {
return Container();
}
},
),
I'm in chicken-egg situation. Thanks.

You need to use some sort of state management. I would use provider. How you would accomplish this is you would have an app level Model that extends ChangeNotifier called languageModel or settingsModel. The model would look something like this:
class SettingsModel extends ChangeNotifier {
Language _lang;
Language get lang => _lang;
set lang(Language lang) {
_lang= lang;
notifyListeners();
}
}
Then in page two when you get your language just call:
Provider.of<SettingsModel>(context, listen: false).lang = yourSelected Language
Then in your ListView you can retrieve the language by the following:
calling Provider.of<SettingsModel>(context).lang or by using a Consumer Widget as shown in the first link.
I hope this helped, and you may need to do more research on state management with provider to implement this, but using some form of state management is the cleanest solution to your problem, so you are not working around the framework instead of letting it help you.

Related

Flutter/Dart - Edit Page in PageView, then Refresh and Scroll back to Same Place?

Currently, I can submit edits to a single page in a PageView and then either Navigator.push to a newly created single edited page or Navigator.pop back to the original Pageview containing the unedited page.
But I'd prefer to pop back to the the same place in an updated/refreshed Pageview. I was thinking I could do this on the original PageView page:
Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
But after editing, how can I pop back to a refreshed PageView which is scrolled to the now updated original page? Or is there a better way? Someone mentioned keys, but I've not yet learned to use them.
The question deals with the concept of Reactive App-State. The correct way to handle this is through having an app state management solution like Bloc or Redux.
Explanation: The app state takes care of the data which you are editing. the EditPage just tells the store(App-State container) to edit that data and the framework takes care of the data that should be updated in the PageView.
as a temporary solution you can use an async call to Navigation.push() and refresh the PageView State once the EditPage comes back. you can also use an overloaded version of pop() to return a success condition which aids for a conditional setState().
Do you know that Navigator.pushReplacement(...) returns a Future<T> which completes when you finally return to original context ?
So how are you going to utilize this fact ?
Lets say you want to update a String of the original page :
String itWillBeUpdated="old value";
#override
Widget build(BuildContext ctx)
{
.
.
.
onPressesed:() async {
itWillBeUpdated= await Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
setState((){});
},
}
On your editing page , you can define Navigator.pop(...) like this :
Navigator.pop<String>(context, "new string");
by doing this , you can provide any data back to the original page and by calling setState((){}) , your page will reflect the changes
This isn't ideal, but works somewhat. First I created a provider class and added the following;
class AudioWidgetProvider with ChangeNotifier {
int refreshIndex;
setRefreshIndex (ri) {
refreshIndex = ri;
return refreshIndex;
}
}
Then in my PageView Builder on the first page, I did this;
Widget build(context) {
var audioWidgetProvider = Provider.of<AudioWidgetProvider>(context);
return
PreloadPageView.builder(
controller: PreloadPageController(initialPage: audioWidgetProvider.refreshIndex?? 0),
Then to get to the EditPage (2nd screen) I did this;
onPressed: () async {
audioWidgetProvider.setRefreshIndex(currentIndex);
Navigator.pushReplacement(context,new MaterialPageRoute(builder: (BuildContext context) => EditPage()),); }
And finally I did this to return to a reloaded PageView scrolled to the edited page;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>HomePage()));
The only problem now is that the PageView list comes from a PHP/Mysql query and I'm not sure what to do if new items are added to the list from the Mysql database. This means the currentIndex will be wrong. But I guess that's the topic of another question.

Provider not accesable when Navigate to new screen

have a problem that I'm sitting on couple of days now.
have an app where:
depending of AUTH state, 'LoginScreen' or 'MainScreen' is Shown.
in MainScreen I setUp bottomNavigation with screens (HomeScreen, ShoppingScreen,MyFavorites)
I set up there as well my StreamProviders(those depend on Auth) by using MultiProvider
on HomeScreen when I User Provider.of(context) it works like it should
but when I use :
`Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProfileScreen(),
),
);
` and use Provider.of(context) there I get "Could not find correct Provider....above this...widget"
I read some issues on that and solution there was to decler providers above MaterailApp which in my case I can not do because I can set up thoese only after Auth is successfull.
Tryed passing context(from HomeScreen) to ProfileScreen(through constructor) and that work but when value changed of UserData it did not update the screen (guessing beacause of diffrent 'contexts')
What am I doing wrong in here,any Ideas?:S
Providers are "scoped".
This means that if they are placed inside a screen, they aren't accessible outside that screen.
Which means that if a provider is scoped but needs to be accessed outside of the route it was created in, we have two solutions:
un-scope the provider. This involves moving the provider to a common ancestor of both widgets that needs to obtain the value.
If those two widgets are on two different Routes, then it basically mean "move the provider above MaterialApp/CupertinoApp.
manually pass the provider to the new screen (needed when using Navigator.push)
The idea is, instead of having one provider, we have two of them, both using the same value as explained here See How to scope a ChangeNotifier to some routes using Provider? for a practical example.
For Navigator.push, this can look like:
final myModel = Provider.of<MyModel>(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ChangeNotifierProvider.value(
value: myModel,
child: MyScreen(),
),
),
);
Please make sure that you application's root widget is Provider Widget, it should event be the parent of MaterialWidget. If this is already the case I will need your code to look into. Something like this
class AppState {
User loggedInUser;
bool get isLoggedIn {
return loggedInUser != null;
}
// Other states as per the requirements
// ...
}

Difference between flutter onTap() and javascript/jquery $(this) in click listener

I'm still in the process of learning dart/flutter. I'm experimenting with ListView/ListTile and specifically onTap() from ListTile.
I'm somewhat familiar with JS/Jquery and am having trouble understanding the differences in approach between JS and flutter.
Please try to ignore any semantic or technical errors in the code below. I'm more concerned with theory/approach than if this is a perfectly formatted and syntactically correct example.
In JS, it's common to do something like make an AJAX call to an API to get a list of items for sale from a database. Assume the API returned 4 fields (primary key ID, isAvailable, item name, and item price). You then process the list and create/insert DOM elements for display.
<div id="234" data-isavailable="y" class=".itemsForSale">TV: $800</div>
<div id="345" data-isavailable="y" class=".itemsForSale">Microwave: $200</div>
<div id="456" data-isavailable="n" class=".itemsForSale">Book: $30</div>
<div id="567" data-isavailable="y" class=".itemsForSale">Sofa: $450</div>
You can then set listeners arbitrarily. For instance, you could do...
$( ".itemsForSale" ).click(function() {
// do something
});
The click handler gets applied in "real-time". The code executed is aware of what was clicked and can analyze/interact with that item in the list. So, you can do something like:
$( ".itemsForSale" ).click(function() {
var isAvailable = $(this).attr('data-isavailable');
if( isAvailable == 'n' )
alert('sorry but that item is out of stock!');
else
addItemToCart( $(this).attr('id') );
});
The point being, that the click handler doesn't necessarily know or care about the underlying data of each item. It becomes "context" aware during runtime and then pulls the relevant values out of the UI/etc.
With flutter, I'm looking to recreate similar functionality using a ListView and ListTile. Specifically, ListTile's onTap(). I'm getting confused because it seems like everything is coded "ahead of time".
Here is a basic example (ignore for now I'm not displaying price/etc)...
import 'package:flutter/material.dart';
class SecondScreen extends StatelessWidget {
var mapper = {
'234': 'TV',
'345': 'Microwave',
'456': 'Book',
'567': 'Sofa'
};
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body:
ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: mapper.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
key: new Key(mapper.keys.elementAt(index).toString()),
title: Text('${mapper.values.elementAt(index)}'),
onTap: () {
print('you clicked: ' + mapper.keys.elementAt(index).toString() );
addToShoppingCart( mapper.keys.elementAt(index).toString() ); // this doesnt exist yet and is just an example
}
);
}
)
);
}
}
First of all, I'm not even sure I need to assign a custom key to each ListView item (based on the database's primary key), but that's irrelevant for now.
What I'm getting stuck on is that the onTap handler doesn't seem to have an equivalent of "$(this)". Does flutter give each ViewTile it's "own" copy of onTap() and that each relevant key info is "hardcoded" into each one (for each itemBuilder loop, the current value of mapper.keys.elementAt(index).toString() is substituted into onTap() )?
I'm probably not describing my confusion properly and I'm not even sure all of the issues I'm going to run into. I'm sure my way of doing this is very "old" and not aligned with react and other newer approaches/frameworks (combining data within the UI elements).
I'm just having trouble seeing how I'm going to be able to add more complex logic (like checking availability of what was clicked).
Does anyone have any references or explanations that help me bridge this gap? I don't even know how to describe the concept of $(this) and how I would search for it in comparison to flutter/etc. I just know that something feels very different.
Thanks!
I understand your confusion probably because I'd a similar question when I started with Flutter just a few months back. And here is what I think -
It doesn't really matter whether the ListTile item has it's own copy of onTap() or not. A. It does not have it's own copy. (Following code snippet will explain) and B. Every programming language / SDK / whatever has its own way of working. What you are trying to say, probably, is that you've a BMW. It has got certain type of breaks. And you are trying to make exact type of break in Audi. It may not be right to do it as the other systems related to the breaks may not work in the optimised way.
Now, look at the following code snippet :
#override
Widget build(BuildContext context) {
return ListView.separated(
separatorBuilder: (context, index) => ListViewDivider(),
itemCount: faceRecTransactions.length,
itemBuilder: (BuildContext context, int index) {
return FacerecTransactionListItem(facerecTransaction: faceRecTransactions[index], onTap:()=> _onTap(faceRecTransactions[index],context),);
},
);
}
void _onTap(FacerecTransaction facerecTransaction, BuildContext context) {
print('Status : ${facerecTransaction.userId}');
Navigator.push(context, MaterialPageRoute(builder: (context) => FacerecDetailPage(
facerecTransaction: facerecTransaction,
criminalList: this.criminalList,)));
}
There's no copy of onTap for every list item. It just 'feels' as it has because we write onTap inside ListView. If you look at the example above, when the user taps on certain item, then the onTap receives the information. We don't create array of onTap as such.
Since, there's no copy of onTaps, it's not really ahead of time code. It's pretty much works like in Ajax where the onTap doesn't really care about the payload. It just does the action(s) specified.
Now, in the above example, the Detailed Page can check the availability of the particular item. And if the item is not available then either appropriate message can be shown. Depending on the requirement, we can either write this code within onTap or on the next page. In short, the onTap can handle the complex logic as you need.

Flutter - Keep page static throughout lifecycle of app?

I have created an AppDrawer widget to wrap my primary drawer navigation and reference it in a single place, like so:
class AppDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Drawer(
child: new ListView(
children: <Widget>[
new ListTile(
title: new Text("Page1"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => Page1.singleInstance));
}
),
new ListTile(
title: new Text("Page2"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new Page2("Page 2")));
}
),
]
),
);
}
}
I have also created a custom AppScaffold widget, which simply returns a consistent AppBar, my custom AppDrawer, and body:
class AppScaffold extends StatelessWidget {
final Widget body;
final String pageTitle;
AppScaffold({this.body, this.pageTitle});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(title: new Text(pageTitle), backgroundColor: jet),
drawer: AppDrawer(),
body: body
);
}
}
I have created two pages: Page1, and Page2. They are simple right now, and look something like this:
class Page1 extends StatelessWidget {
final String pageText;
Page1(this.pageText);
static Page1 get singleInstance => Page1("Page1");
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeCustomWidget())
],
)
),
);
}
}
class Page2 extends StatelessWidget {
final String pageText;
Page2(this.pageText);
#override
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeOtherCustomWidget())
],
)
),
);
}
}
When I run my app, I can see the navbar and drawer correctly. I can click on the links in the drawer to navigate between my pages. However, each time I navigate to a page, all of the widgets on that page get reset to their initial state. I want to ensure that the widgets do not get reset. Another way to think of this is: I only want one instance of each page throughout the lifecycle of the app, instead of creating them new whenever a user navigates to them.
I tried creating a static instance of Page1 that the Drawer uses when the onTap event is fired, but this does not work. Am I thinking about this incorrectly? Do I need to convert to a Stateful widget?
Oh, you're in for a treat... This will be kinda long (sorry) but please read all of it before making decisions and taking action - I promise I am saving you time.
There are many different solutions to this problem, but in general what you're asking about is state management (which is really software engineering, more info here - Understanding state management, and why you never will).
I'll try my best to explain what is happening in your specific case...
Problem:
Think of Navigator as a List of application states, which you can manipulate via its various methods (i.e. pop(), push(), etc.), with this in mind it is clear what is happening - on a button press you're actually removing the current state (page) and right after that you're pushing a new instance of your state (page).
Solution(s):
As I said, there are many solutions to this problem, for example, you may be tempted to store the state (the changes you made to a particular "page") somewhere in a var and inject that var when navigating between "pages", when creating a new instance of that page, but you'll soon run into other problems. This is why I don't think anyone can provide a simple solution to this problem...
First, may I suggest you some useful reads on the matter:
Flutter official docs on state management - When you get to the "Options" section of this, the fun part begins and can quickly get overwhelming, but fear not :P
Be sure to read the medium article mentioned in the start of my answer too, I found it really helpful.
These reads will be more than enough to help you make a decision, plus there are a ton of articles on Medium and YouTube videos touching on the matter of state management with Flutter (even some from the authors of the framework) - just search for "State management with Flutter".
Now my own personal opinion:
If it's a really simple use case and you don't plan to grow (which is almost never the case, trust me), you can just use StatefulWidgets in combination with setState() and maybe InheritedWidget (for dependency injection down the tree, or like React guys call it "lifting state up"). Or instead of the above, maybe have a look at scoped_model, which kinda abstracts all of this for you (tho, I haven't played with it).
What I use right now for a real world project is bloc and flutter_bloc (BLoC = Business Logic Component), I will not get into the details of it, but basically it takes the idea of scoped_model one step further, without over-complicating abstractions. bloc is responsible for abstracting away the "business logic" of your application and flutter_bloc to "inject" the state in your UI and react to state changes (official Flutter position on the matter is that UI = f(State)).
A BLoC has an input and an output, it takes in events as an input (can be user input, or other, any type of event really) and produces a state. In summary that's it about bloc.
A great way to get started is BLoC's official documentation. I highly recommend it. Just go through everything.
(p.s. This may be my personal opinion, but in the end state management in Flutter is all based on some form of using InheritedWidget and setState() in response to user input or other external factors that should change the application state, so I think the BLoC pattern is really on point with abstracting those :P)

I am losing stream data when navigating to another screen

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.