these my two classes(two pages). these two classes open multiple times.
I put debug point in futurebuilder in two classes.
debug point running,
MainCategory page and got to the next page
SubCategory page and again running MainCategory page(previous page) futurebuilder and again running MainCategory page futurebuilder
navigate subcategory page to third page running subcategory page and main category page
I upload my two classes to GitHub and please let me know what the issue is.
MainCategory code: https://github.com/bhanuka96/ios_login/blob/master/MainCategory.dart
SubCategory code: https://github.com/bhanuka96/ios_login/blob/master/subCategory.dart
As stated in the documentation, you should not fetch the Future for the Futurebuilder during the widget's build event.
https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateConfig, or
State.didChangeDependencies. It must not be created during the
State.build or StatelessWidget.build method call when constructing the
FutureBuilder. If the future is created at the same time as the
FutureBuilder, then every time the FutureBuilder's parent is rebuilt,
the asynchronous task will be restarted.
So, try to move your call to getRegister method outside the build method and replace it with the returned Future value.
For example, below I have a class that returns a Future value which will be consumed with the help of FutureBuilder.
class MyApiHelper{
static Future<List<String>> getMyList() async {
// your implementation to make server calls
return List<String>();
}
}
Now, inside your widget, you will have something like this:
class _MyHomePageState extends State<MyHomePage> {
Future<List<String>> _myList;
#override
void initState() {
super.initState();
_myList = MyApiHelper.getMyList();
}
#override
Widget build(BuildContext context) {
return Scaffold(body: FutureBuilder(
future: _myList,
builder: (_, AsyncSnapshot<List<String>> snapLs) {
if(!snapLs.hasData) return CircularProgressIndicator();
return ListView.builder(
itemCount: snapLs.data.length,
itemBuilder: (_, index) {
//show your list item row here...
},
);
},
));
}
}
As shown above, the Future is fetched in the initState function and used inside the build method and used by FutureBuilder.
I hope this was helpful.
Thanks.
If you happen to use Provider, here's (in my opinion) a clearer alternative based on your question:
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureProvider<List<String>>(
create: (_) => MyApiHelper.getMyList(),
child: Consumer<List<String>>(
builder: (_, list, __) {
if (list == null) return CircularProgressIndicator();
return ListView.builder(
itemCount: list.length,
itemBuilder: (_, index) {
//show your list item row here...
},
);
};
),
);
}
}
This can also be achieved of course as a StatefulWidget as suggested by the other answer, or even with flutter_hooks as explained in Why is my Future/Async Called Multiple Times?
You can create new Widget and pass Function to
returnFuture as
() {
return YourFuture;
}
import 'dart:developer';
import 'package:flutter/material.dart';
class MyFutureBuilder<T> extends StatefulWidget {
final Future<T> Function() returnFuture;
final AsyncWidgetBuilder<T> builder;
final T initialData;
MyFutureBuilder({
this.returnFuture,
#required this.builder,
this.initialData,
Key key,
}) : super(key: key);
#override
_MyFutureBuilderState<T> createState() => _MyFutureBuilderState<T>();
}
class _MyFutureBuilderState<T> extends State<MyFutureBuilder<T>> {
bool isLoading = false;
Future<T> future;
#override
void initState() {
super.initState();
future = widget.returnFuture();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
builder: widget.builder,
initialData: widget.initialData,
future: future,
);
}
}
Example
MyFutureBuilder<List<User>>(
returnFuture: () {
return moderatorUserProvider
.getExecutorsAsModeratorByIds(val.users,
save: true);
},
builder: (cont, asyncData) {
if (asyncData.connectionState !=
ConnectionState.done) {
return Center(
child: MyCircularProgressIndicator(
color: ModeratorColor.executors.color,
),
);
}
return Column(
children: asyncData.data
.map(
(singlExecutor) =>
ChooseInfoButton(
title:
'${singlExecutor.firstName} ${singlExecutor.secondName}',
subTitle: 'Business analyst',
middleText: '4.000 NOK',
subMiddleText: 'full time',
label: 'test period',
subLabel: '1.5 month',
imageUrl:
assetsUrl + 'download.jpeg',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) =>
ModeratorExecutorEditPage(),
),
);
},
),
)
.toList());
},
)
```
Related
I am very beginner to Flutter and Dart. So I am trying to update the state of the parent widget, but to be honest after trying many different solutions none worked for me, or am I doing something wrong?
What I'm trying to do is to update the _title in _BooksState() when the page changes in _Books() class.
How do I set the _title state from the child (_Books()) widget?
class Books extends StatefulWidget {
#override
_BooksState createState() {
return _BooksState();
}
}
class _BooksState extends State<Books> {
String _title = 'Books';
_setTitle(String newTitle) {
setState(() {
_title = newTitle;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
),
body: _Books(),
);
}
}
class _Books extends StatelessWidget {
final PageController _controller = PageController();
final Stream<QuerySnapshot> _stream =
Firestore.instance.collection('Books').orderBy('title').snapshots();
_setAppBarTitle(String newTitle) {
print(newTitle);
// how do I set _title from here?
}
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _stream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
final books = snapshot.data.documents;
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
return PageView.builder(
controller: _controller,
scrollDirection: Axis.horizontal,
itemCount: books.length,
itemBuilder: (context, index) {
final book = books[index];
return ListTile(
title: Text(book['title']),
subtitle: Text(book['author']),
);
},
onPageChanged: (index) {
_setAppBarTitle(books[index].data['title']);
},
);
}
},
);
}
}
let me repeat your question in other words: You want to setstate a widget(or refresh a page, or change a variable 'binded' to a widget) when something happens(not inside the same class of the widget).
This is a common problem for all newbies in flutter(including me), which is called state management.
Of course you can always put everything inside the same dart file, or even the same class, But we don't do that for larger app.
In order to solve this problem, I created 2 examples:
https://github.com/lhcdims/statemanagement01
This example uses a timer to check whether something inside a widget is changed, if so, setstate the page that the widget belongs to.
try to take a look at the function funTimerDefault() inside main.dart
Ok, this was my first try, not a good solution.
https://github.com/lhcdims/statemanagement02
This example's output is the same as 1, But is using Redux instead of setState. Sooner or later you'll find that setstate is not suitable for all cases(like yours!), you'll be using Redux or BLoC.
Read the readme inside the examples, build and run them, you'll then be able to (refresh) any widget(or changes variables binded to a widget), at any time(and anywhere) you want. (even the app is pushed into background, you can also try this in the examples)
What you can do is move you _Books class inside the _BooksState class..
And instead of using _Books as class you can use it as Widget inside _BooksState class so that you can access the setState method of StatefulWidget inside the Widget you create.
I do it this way and even I'm new to Flutter and Dart...This is working for me in every case even after making an API call..I'm able to use setState and set the response from API.
Example:
class Books extends StatefulWidget {
#override
_BooksState createState() {
return _BooksState();
}
}
class _BooksState extends State<Books> {
String _title = 'Books';
_setTitle(String newTitle) {
setState(() {
_title = newTitle;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
),
body: _books(), // Using the Widget here
);
}
// Your `_Books` class created as `Widget` for setting state and new title.
Widget _books() {
final PageController _controller = PageController();
final Stream<QuerySnapshot> _stream =
Firestore.instance.collection('Books').orderBy('title').snapshots();
_setAppBarTitle(String newTitle) {
print(newTitle);
// how do I set _title from here?
// Since you created this method and setting the _title in this method
// itself using setstate you can directly pass the new title in this method..
_setTitle(newTitle);
}
return StreamBuilder<QuerySnapshot>(
stream: _stream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
final books = snapshot.data.documents;
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
return PageView.builder(
controller: _controller,
scrollDirection: Axis.horizontal,
itemCount: books.length,
itemBuilder: (context, index) {
final book = books[index];
return ListTile(
title: Text(book['title']),
subtitle: Text(book['author']),
);
},
onPageChanged: (index) {
_setAppBarTitle(books[index].data['title']);
},
);
}
},
);
}
}
I am using sqflite database to save user list.
I have user list screen, which shows list of user and it has a fab button,
on click of fab button, user is redirected to next screen where he can add new user to database.
The new user is properly inserted to the database
but when user presses back button and go backs to user list screen,
the newly added user is not visible on the screen.
I have to close the app and reopen it,then the newly added user is visible on the screen.
I am using bloc pattern and following is my code to show user list
class _UserListState extends State<UserList> {
UserBloc userBloc;
#override
void initState() {
super.initState();
userBloc = BlocProvider.of<UserBloc>(context);
userBloc.fetchUser();
}
#override
void dispose() {
userBloc?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).pushNamed("/detail");
},
child: Icon(Icons.add),
),
body: StreamBuilder(
stream: userBloc.users,
builder: (context, AsyncSnapshot<List<User>> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data != null) {
return ListView.builder(
itemBuilder: (context, index) {
return Dismissible(
key: Key(snapshot.data[index].id.toString()),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
userBloc.deleteParticularUser(snapshot.data[index]);
},
child: ListTile(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => UserDetail(
user: snapshot.data[index],
)));
},
title: Text(snapshot.data[index].name),
subtitle:
Text("Mobile Number ${snapshot.data[index].userId}"),
trailing:
Text("User Id ${snapshot.data[index].mobileNumber}"),
),
);
},
itemCount: snapshot.data.length,
);
}
},
),
);
}
}
Following is my bloc code
class UserBloc implements BlocBase {
final _users = BehaviorSubject<List<User>>();
Observable<List<User>> get users => _users.stream;
fetchUser() async {
await userRepository.initializeDatabase();
final users = await userRepository.getUserList();
_users.sink.add(users);
}
insertUser(String name,int id,int phoneNumber) async {
userRepository.insertUser(User(id, name, phoneNumber));
fetchUser();
}
updateUser(User user) async {
userRepository.updateUser(user);
}
deleteParticularUser(User user) async {
userRepository.deleteParticularUser(user);
}
deleteAllUser() {
return userRepository.deleteAllUsers();
}
#override
void dispose() {
_users.close();
}
}
As Remi posted answer saying i should try BehaviorSubject and ReplaySubject which i tried but it does not help. I have also called fetchUser(); inside insertUser() as pointed in comments
Following is the link of the full example
https://github.com/pritsawa/sqflite_example
Follow up from the comments, it seems you don't have a single instance of your UsersBloc in those two pages. Both the HomePage and UserDetails return a BlocProvider which instantiate a UsersBloc instance. Because you have two blocs instances(which you shouldn't have) you don't update the streams properly.
The solution is to remove the BlocProvider from those two pages(HomePage and UserDetail) and wrap the MaterialApp widget in it to make the same instance available to both pages.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: UserBloc(),
child:MaterialApp(...
The HomePage will be:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return UserList(),
);
}
}
Remove the BlocProvider from UserDetail as well.
In the UsersBloc then call fetchUser() inside the insertUser() method after the user insertion, to requery the database and update the users stream.
Also as RĂ©mi Rousselet said, use one of the subjects that return previous values.
The issue is that you're using a PublishSubject.
When a new listener subscribes to a PublishSubject, it does not receive the previously sent value and will only receive the next events.
The easiest solution is to use a BehaviorSubject or a ReplaySubject instead. These two will directly call their listener with the latest values.
#override
void initState() {
super.initState();
userBloc = BlocProvider.of<UserBloc>(context);
userBloc.fetchUser();
}
The problem is that you have called the userBloc.fetchUser() function in the initState of the page.
Bloc stream emits whenever a new data is added to it and the userBloc.fetchUser() function does exactly that, it adds the userList that you fetch from the Sqflite database.
Whenever you come back to the userlist screen from add user screen, init function is NOT called. It is only called when the userlist screen is created, that is, whenever you push it to the navigation stack.
The workaround is to call userBloc.fetchUser() whenever your StreamBuilder's snapshot data is null.
...
if (!snapshot.hasData) {
userBloc.fetchUser();
return Center(
child: CircularProgressIndicator(),
);
}
...
Currently I get the BuildContext from the build method in HomeScreen, and then I have to pass it down to _gridSliver then down to _storeCard.
How can I write the code so that I don't need to pass the context down?
Maybe I can create a new private StatelessWidget called _StoreCard that will have its own build method and thus its own BuildContext?
class HomeScreen extends StatelessWidget {
HomeScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StoreConnector<AppState, List<MyStore.Store>>(
converter: (Store<AppState> store) => store.state.home.stores,
builder: (BuildContext context, List<MyStore.Store> stores) =>
CustomScrollView(slivers: <Widget>[_gridSliver(stores, context)]));
}
Widget _gridSliver(stores, context) {
return SliverGrid(
delegate: SliverChildListDelegate(List<Widget>.from(stores.map(_storeCard, context))));
}
Widget _storeCard(MyStore.Store store, BuildContext context) {
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => StoreScreen(storeId: store.id)),
);
},
child: Container(child: Text(store.name))
);
}
}
Another instance of this problem is I navigate on a child function.
#override
Widget build(BuildContext context) {
return Column(
children: [
WhiteButton(text: "Login with Facebook", onPressed: _loginWithFacebook),
WhiteButton(text: "Login with Google", onPressed: _loginWithGoogle),
])
)
}
_loginWithFacebook(context) async {
...
var user = User.fromFacebook(result.accessToken.token, json.decode(graphResponse.body));
await _login(user, context);
}
}
_loginWithGoogle(context) async {
...
GoogleSignInAccount googleUser = await _googleSignIn.signIn();
await _login(User.fromGoogle(googleUser), context);
}
_login(user, context) async {
var fetchedUser = await MeService.getUser(user);
if (fetchedUser != null) {
loginSuccess(fetchedUser);
Navigator.popUntil(context, ModalRoute.withName(MainRoutes.root));
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => RegisterScreen(user: user)),
);
}
}
To get a new BuildContext, you have two main solutions:
Extract part of the subtree into a new widget, typically StatelessWidget. And then use it's BuildContext from the build method
Use Builder widget, which is basically a reusable widget made to obtain a BuildContext:
Example:
#override
Widget build(BuildContext context) {
return Builder(
builder: (context) {
// do something with this new context
},
);
}
You have to use a Bloc pattern that uses an Inherited Widget, but still you'll have to pass context, but in a more straight forward way. I recommend using this app by Stephen Grider, to figure out how the whole thing works. He explains in his tutorial how to put the whole thing together but I can't link you to that because that would be advertising.
The idea is, you first create a file Bloc.dart that is going to contain your logic, then you create what is called a Provider, in a Provider.dart.
Provider.dart:
class Provider extends InheritedWidget {
final bloc = Bloc();
Provider({Key key, Widget child}) : super(key: key, child: child);
bool updateShouldNotify(_) => true;
static Bloc of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(Provider) as Provider).bloc;
}
}
In your file that contains the Material App, you wrap the material App with the provider:
Widget build(BuildContext context) {
return Provider(
child: MaterialApp(
And then you use the provider in every other class down the three of widgets.
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final bloc = Provider.of(context); // this is where you insert the provider
return StoreConnector<AppState, List<MyStore.Store>>(
converter: (Store<AppState> store) => store.state.home.stores,
builder: (BuildContext context, List<MyStore.Store> stores) =>
CustomScrollView(slivers: <Widget>[_gridSliver(stores, context)]));
}
Widget _gridSliver(stores) {
final bloc = Provider.of(context);
return SliverGrid(
delegate: SliverChildListDelegate(List<Widget>.from(stores.map(_storeCard, context))));
}
Widget _storeCard(MyStore.Store store) {
final bloc = Provider.of(context);
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => StoreScreen(storeId: store.id)),
);
},
child: Container(child: Text(store.name))
);
}
}
I'm a total noob with flutter and take everything with grain of salt, but this is what I would use. Hope it helps.
I'm currently working through learning Flutter. I'm starting with trying to build the basic list app. The current flow I have is my Stateful TasksManager Widget saving user input to the state, and then pushing it to the _taskList List, which I have being sent over to a Stateless TaskList widget, which is rendering the list.
I expect the list view to be updated with the new task after the "Save" button is clicked, but what I'm getting is after I "Save", the list view only updates during the subsequent change of state. For example, if I were to "Save" the string "Foo" to add to the list, I'm only seeing that update in the view after I go to type in another item, such as "Bar".
TaskManager
class TasksManager extends StatefulWidget{
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return TasksManagerState();
}
}
class TasksManagerState extends State<TasksManager>{
final List _taskList = [];
String _newTask = '';
#override
Widget build(BuildContext context) {
// TODO: implement build
return Column(
children: <Widget>[
TextField(
onChanged: (value){
setState(() {
_newTask = value;
});
print(_taskList);
},
),
RaisedButton(
child: Text('Save'),
onPressed: () => _taskList.add(_newTask),
),
Expanded(child: TaskList(_taskList))
],
);
}
}
TaskList
class TaskList extends StatelessWidget {
final List taskList;
TaskList(this.taskList);
Widget _buildListItem(BuildContext context, int index) {
return ListTile(
title: Text(taskList[index]),
);
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return ListView.builder(
itemBuilder: _buildListItem,
itemCount: taskList.length,
);
}
}
Yes, Flutter only updates when it is instructed to.
setState() marks the widget dirty and causes Flutter to rebuild:
onPressed: () => setState(() => _taskList.add(_newTask)),
I want to update my ListView if i remove or add items. Right now i just want to delete items and see the deletion of the items immediately.
My application is more complex so i wrote a small example project to show my problems.
The TestItem class holds some data entries:
class TestItem {
static int id = 1;
bool isFinished = false;
String text;
TestItem() {
text = "Item ${id++}";
}
}
The ItemInfoViewWidget is the UI representation of the TestItem and removes the item if it is finished (whenever the Checkbox is changed to true).
class ItemInfoViewWidget extends StatefulWidget {
TestItem item;
List<TestItem> items;
ItemInfoViewWidget(this.items, this.item);
#override
_ItemInfoViewWidgetState createState() =>
_ItemInfoViewWidgetState(this.items, this.item);
}
class _ItemInfoViewWidgetState extends State<ItemInfoViewWidget> {
TestItem item;
List<TestItem> items;
_ItemInfoViewWidgetState(this.items, this.item);
#override
Widget build(BuildContext context) {
return new Card(
child: new Column(
children: <Widget>[
new Text(this.item.text),
new Checkbox(
value: this.item.isFinished, onChanged: isFinishedChanged)
],
),
);
}
void isFinishedChanged(bool value) {
setState(() {
this.item.isFinished = value;
this.items.remove(this.item);
});
}
}
The ItemViewWidget class builds the ListView.
class ItemViewWidget extends StatefulWidget {
List<TestItem> items;
ItemViewWidget(this.items);
#override
_ItemViewWidgetState createState() => _ItemViewWidgetState(this.items);
}
class _ItemViewWidgetState extends State<ItemViewWidget> {
List<TestItem> items;
_ItemViewWidgetState(this.items);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text('Test'),
),
body: ListView.builder(
itemCount: this.items.length,
itemBuilder: (BuildContext context, int index) {
return new ItemInfoViewWidget(this.items, this.items[index]);
}),
);
}
}
The MyApp shows one TestItem and a button that navigates to the ItemViewWidget page.
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
List<TestItem> items = new List<TestItem>();
_MyHomePageState() {
for (int i = 0; i < 20; i++) {
this.items.add(new TestItem());
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Column(
children: <Widget>[
ItemInfoViewWidget(this.items, this.items.first),
FlatButton(
child: new Text('Open Detailed View'),
onPressed: buttonClicked,
)
],
));
}
void buttonClicked() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ItemViewWidget(this.items)),
);
}
}
If i toggle the Checkbox of the first item, the Checkbox is marked as finished (as expected), but it is not removed from the UI - however it is removed from the list.
Then I go back to the Main page and I can observe that Item 1 is checked there as well.
So if I go to the ItemViewWidget page again, I can observe that the checked items are no longer present.
Based on these observations, I come to the conclusion that my implementation works, but my UI is not updating.
How can I change my code to make an immediate update of the UI possible?
Edit: This is not a duplicate, because
I dont want to create a new instance of my list just to get the UI updated.
The answer does not work: I added this.items = List.from(this.items); but the behavior of my app is the same as already described above.
I don't want to break my reference chain by calling List.from, because my architecture has one list that is referenced by several classes. If i break the chain i have to update all references by my own. Is there a problem with my architecture?
I dont want to create a new instance of my list just to get the UI updated.
Flutter uses immutable object. Not following this rule is going against the reactive framework. It is a voluntary requirement to reduce bugs.
Fact is, this immutability is here especially to prevents developers from doing what you currently do: Having a program that depends on sharing the same instance of an object between classes; as multiple classes may want to modify it.
The real problem lies in the fact that it is your list item that removes delete an element from your list.
The thing is since it's your item which does the computing, the parent is never notified that the list changed. Therefore it doesn't know it should rerender. So nothing visually change.
To fix that you should move the deletion logic to the parent. And make sure that the parent correctly calls setState accordingly. This would translate into passing a callback to your list item, which will be called on deletion.
Here's an example:
class MyList extends StatefulWidget {
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
List<String> list = List.generate(100, (i) => i.toString());
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
return MyItem(list[index], onDelete: () => removeItem(index));
},
);
}
void removeItem(int index) {
setState(() {
list = List.from(list)
..removeAt(index);
});
}
}
class MyItem extends StatelessWidget {
final String title;
final VoidCallback onDelete;
MyItem(this.title, {this.onDelete});
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(this.title),
onTap: this.onDelete,
);
}
}