I have an app that routes to a "mainapp" page after logging in. This app contains a bottom navigation bar which displays pages of the corresponding pressed icon. I want to pass data of type Map<String, dynamic> to these pages but I am having trouble. This map is generated from a function that fetches the data from a server, saves it to shared preferences, then loads the shared preferences and returns it as a map (all contained in getData()). I want to pass this map around so I don't have to load shared preferences each time, but will also update this map along with shared preferences when needed( possibly an action on one of the pages).
class MainApp extends StatefulWidget {
#override
_MainAppState createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
Map<String, dynamic> Data;
StartFunc() async {
Data = await getData();
setState(() {});
}
#override
void initState() {
StartFunc();
super.initState();
}
var _pages = [
PageOne(Data:Data),
PageTwo(),
PageThree(),
PageFour(),
PageFive(),
];
int _currentIndex = 0;
onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
return _currentIndex == 2
? PageTwo()
: Scaffold(
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.library_books), title: Text('')),
BottomNavigationBarItem(
icon: Icon(Icons.notifications), title: Text('')),
BottomNavigationBarItem(
icon: Icon(Icons.add_circle_outline), title: Text('')),
BottomNavigationBarItem(
icon: Icon(Icons.mail), title: Text('')),
BottomNavigationBarItem(
icon: Icon(Icons.person), title: Text('')),
],
onTap: onTabTapped,
currentIndex: _currentIndex,
),
);
}
}
I'm getting an error saying Only static members can be accessed in initializers. I was wondering if inherited widgets or other design patterns such as scoped model and BLoC can help but not sure if that's the right way to go. I'm also not sure how I would start implementing them in this case.
There are two problems in your code:
using an async method in the body of initState()
see here for details
using instance data in an initializer
see here for details
What follow is a very basic rewrite of your code, with minimal corrections.
The data map is loaded from a mocked backend, updated inside PageOne and printed to console in PageTwo onTap callback.
Please note that I've changed instance variable Data to data to be compliant with Effective Dart guidelines.
Note that the gist does not properly addresses the synchronization of the backend service with the shared preferences: this is something that have probably to be accounted in the final product.
I just commented what it is necessary to get your code works:
if the complexity of your system and the relations with external API start growing it could be worth considering a Bloc architecture.
import 'package:flutter/material.dart';
void main() => runApp(new MainApp());
// Mock up of an async backend service
Future<Map<String, dynamic>> getData() async {
return Future.delayed(Duration(seconds: 1), () => {'prop1': 'value1'});
}
class PageOne extends StatelessWidget {
final Map<String, dynamic> data;
PageOne({Key key, this.data}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: const Text('update preferences'),
onPressed: () {
data['prop2'] = 'value2';
},
),
);
}
}
class PageTwo extends StatelessWidget {
final Map<String, dynamic> data;
PageTwo({Key key, this.data}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: const Text('Got It!'),
onPressed: () {
print("data is now: [$data]");
},
),
);
}
}
class MainApp extends StatefulWidget {
#override
_MainAppState createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
//Map<String, dynamic> Data;
Map<String, dynamic> data;
/*
StartFunc() async {
Data = await getData();
setState(() {});
}
*/
#override
void initState() {
//StartFunc();
super.initState();
getData().then((values) {
setState(() {
data = values;
});
});
}
/*
PageOne(data:data) is an invalid value for an initializer:
there is no way to access this at this point.
Initializers are executed before the constructor,
but this is only allowed to be accessed after the call
to the super constructor.
*/
/*
var _pages = [
PageOne(data:data),
PageTwo(),
];
*/
Widget getPage(int index) {
switch (index){
case 0:
return PageOne(data:data);
break;
case 1:
return PageTwo(data:data);
break;
default:
return PageOne();
break;
}
}
int _currentIndex = 0;
onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
/*
return _currentIndex == 2
? PageTwo()
: Scaffold(
I use a MaterialApp because of material widgets (RaisedButton)
It is not mandatory, but it is mainstream in flutter
*/
return MaterialApp(
title: 'My App',
home: Scaffold(
appBar: AppBar(title: Text("My App Bar")),
body: getPage(_currentIndex),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.first_page), title: Text('')),
BottomNavigationBarItem(
icon: Icon(Icons.last_page), title: Text('')),
],
onTap: onTabTapped,
currentIndex: _currentIndex,
),
));
}
}
Related
I am trying to understand ScopeModel in Flutter and need some help on how access values from the model on a different page
My home page has a bottom navigation bar and when click just display the search page. I have wrap the widget tree with the ScopeModel and added the model.
The count is getting incremented but I am not sure how to access it from the search page
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScopeCounter sc;
#override
void initState() {
super.initState();
sc = new ScopeCounter();
}
final List<Widget> _children = [
..
Search()
];
var _currentIndex = 0;
#override
Widget build(BuildContext context) {
return ScopedModel(
model:sc ,
child: Scaffold(
appBar: AppBar(
title: Center(child: Text("test")),
),
drawer: JobsDrawer(),
body: _children[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: onTabTapped, // new
currentIndex: _currentIndex, // new
items: [
new BottomNavigationBarItem(
icon: new Icon(Icons.search),
title: new Text("search"),
)
])),
);
}
void onTabTapped(int index) {
setState(() {
sc.increment();
print(sc.counter1.count);
_currentIndex = index;
});
}
}
This my Model
class ScopeCounter extends Model {
Counter counter1 = Counter();
increment() {
counter1.count += 1;
}
}
class Counter {
int count = 1;
}
Search page
class Search extends StatefulWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
...
}
}
I would like access the count from the "search" page.
Thanks for your help
You just have to wrap your SearchPage's Widget (Scaffold in this case) with a ScopedModelDescendant Widget. This gives you access to your ScopedModel.
A great explanation can be found in the documentation: https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
EDIT: Also your ScopedModel must be a parent of both: MyHomePage and Search.
I'm trying to dynamically delete simple grid item on long press;
I've tried the most obvious way: created a list of grid data, and called setState on addition or deletion of the item.
UPD: Items works properly in the list, since it's initialisation loop moved to initState() method (just as #jnblanchard said in his comment), and don't generate new items at every build() call, but deletion is still doesn't work.
If it has more items, than can fit the screen, it deletes last row, (when enough items deleted), otherwise the following exception is thrown:
I/flutter (28074): The following assertion was thrown during performLayout():
I/flutter (28074): SliverGeometry is not valid: The "maxPaintExtent" is less than the "paintExtent".
I/flutter (28074): The maxPaintExtent is 540.0, but the paintExtent is 599.3. By definition, a sliver can't paint more
I/flutter (28074): than the maximum that it can paint!
My test code now:
main class
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:options_x_ray_informer/prototyping/TestTile.dart';
class Prototype extends StatefulWidget{
#override
_PrototypeState createState() => _PrototypeState();
}
class _PrototypeState extends State<Prototype> {
//list of grid data
List<Widget> gridItemsList = [];
#override
void initState(){
super.initState();
//----filling the list----
for(int i =0; i<10; i++){
gridItemsList.add(
TestTile(i, (){
//adding callback for long tap
delete(i);
})
);
}
}
#override
Widget build(BuildContext context) {
//----building the app----
return Scaffold(
appBar: AppBar(
title: Text("Prototype"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
int index = gridItemsList.length+1;
add(
new TestTile(index, (){
delete(index);
})
);
},
),
]
),
body: GridView(
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
children: gridItemsList
)
);
}
///method for adding the items
void add(Widget toAdd){
setState(() {
TestTile tile = toAdd as TestTile;
gridItemsList.add(toAdd);
print("tile number#${tile.index} added");
});
}
///method for deleting the items
void delete(int index){
setState(() {
gridItemsList.removeAt(index);
print("tile number#$index is deleted");
});
}
}
and separate widget class for grid items
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class TestTile extends StatelessWidget{
int _index;
var _callback;
TestTile(this._index, this._callback);
get index => _index;
#override
Widget build(BuildContext context) {
return GridTile(
child: Card(
child: InkResponse(
onLongPress: _callback,
child: Center(
child:Text("data#$_index")
)
)
),
);
}
}
How can I delete an item from grid view?
p.s. the provided code is just my attempts of solving the problem - you can offer another way, if you want!
I wrote this up from the example app, it has a few things that you may find useful. Notably I abstract the list data-structure by holding the length of the list inside a stateful widget. I wrote this with a ListView but I think you could change that to a GridView without any hiccups.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(
title: Text("Owl"),
actions: <Widget>[IconButton(icon: Icon(Icons.remove), onPressed: () => this.setState(() => _counter > 1 ? _counter-- : _counter = 0)), IconButton(icon: Icon(Icons.add), onPressed: () => this.setState(() => _counter++))],
),
body: ListView.builder(itemExtent: 50, itemCount: _counter, itemBuilder: (context, index) => Text(index.toString(), textAlign: TextAlign.center, style: Theme.of(context).textTheme.title))
);
}
}
Finally I've got what I wanted.
I'll leave it here for someone who might have the same problem :)
Main class:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:options_x_ray_informer/prototyping/TestTile.dart';
class Prototype extends StatefulWidget{
#override
_PrototypeState createState() => _PrototypeState();
}
class _PrototypeState extends State<Prototype> {
//list of some data
List<Person> partyInviteList = [];
_PrototypeState(){
//filling the list
for(int i=0; i<5; i++){
partyInviteList.add(Person.generateRandomPerson());
}
print("Person ${partyInviteList.toString()}");
}
#override
Widget build(BuildContext context) {
//----building the app----
return Scaffold(
appBar: AppBar(
title: Text("Prototype"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
//generating an item on tap
onPressed: () {
setState(() {
partyInviteList.add(Person.generateRandomPerson());
});
},
),
]
),
body: GridView.count(
crossAxisCount: 2,
children: List.generate(partyInviteList.length, (index) {
//generating tiles with people from list
return TestTile(
partyInviteList[index], (){
setState(() {
print("person ${partyInviteList[index]} is deleted");
partyInviteList.remove(partyInviteList[index]);
});
}
);
})
)
);
}
}
///person class
class Person{
Person(this.firstName, this.lastName);
static List<String> _aviableNames = ["Bob", "Alise", "Sasha"];
static List<String> _aviableLastNames = ["Green", "Simpson", "Stain"];
String firstName;
String lastName;
///method that returns random person
static Person generateRandomPerson(){
Random rand = new Random();
String randomFirstName = _aviableNames[rand.nextInt(3)];
String randomLastName = _aviableLastNames[rand.nextInt(3)];
return Person(randomFirstName, randomLastName);
}
#override
String toString() {
return "$firstName $lastName";
}
}
Support class:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:options_x_ray_informer/prototyping/Prototype.dart';
class TestTile extends StatelessWidget{
final Person person;
var _callback;
TestTile(this.person, this._callback);
#override
Widget build(BuildContext context) {
return GridTile(
child: Card(
child: InkResponse(
onLongPress: _callback,
child: Center(
child:Text("${person.toString()}")
)
)
),
);
}
}
I want to use BottomNavigationBar of Flutter so for that I have created a class called BaseWidget which will be changed as the user taps the item.
class BaseWidget extends StatefulWidget {
final String title;
BaseWidget(this.title);
_BaseWidgetState createState() => _BaseWidgetState(this.title);
}
class _BaseWidgetState extends State<BaseWidget> {
final String title;
_BaseWidgetState(this.title);
#override
Widget build(BuildContext context) {
return Center(child: Text(title));
}
}
In the above class am returning the Center widget with child as Text widget.
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int pageIndex = 0;
final _home = BaseWidget('Home');
final _business = BaseWidget('Business');
final _school = BaseWidget('School');
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: choosePager(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(
icon: Icon(Icons.business), title: Text('Business')),
BottomNavigationBarItem(
icon: Icon(Icons.school), title: Text('School')),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
this.pageIndex = index;
});
}
Widget choosePager() {
switch (pageIndex) {
case 0:
return _home;
break;
case 1:
return _business;
break;
case 2:
return _school;
break;
default:
return Text('Unknown');
break;
}
}
}
Problem 1:
Whenever user taps on the BottomNavigationBarItem the text should change to the respected string passed in the BaseWidget's constructor. But it only shows Home and the rest 2 are ignored.
Problem 2:
I am planning to replace Center widget with the ListView widget to populate the list of Schools and Businesses which will be fetched from the network API in paginated way. So I don't want to reinitialise the classes again when BottomNavigationBarItem is tapped as that would result in loss of data which is already fetched. To prevent data lose I am declaring _home, _business & _school property and using these property in choosePager() method.
There are several issues with your code:
1- The real problem is that you never rebuild the BaseWidget. You construct 3 new BaseWidgets, but you only ever call the build of the _home widget, because it's the first one returned by choosePager(). Since you don't create _home, _business, _school in the HomeWidget build, no other BaseWidget can ever get built.
2- When you don't need to store any state/variables for a widget, use a Stateless widget.
3- Don't do anything in the constructor of your State. Use initState https://docs.flutter.io/flutter/widgets/State/initState.html for that instead.
4- Create widgets using const constructors when possible.
5- Widget constructor take named parameters. One of those should be the key. Use super to call the base constructor.
With that in mind, this is what the code should look like:
class BaseWidget extends StatelessWidget {
final String title;
const BaseWidget({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Text(title),
);
}
}
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int pageIndex = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: choosePager(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('Business'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('School'),
),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
pageIndex = index;
});
}
Widget choosePager() {
Widget result;
switch (pageIndex) {
case 0:
result = BaseWidget(title: 'Home');
break;
case 1:
result = BaseWidget(title: 'Business');
break;
case 2:
result = BaseWidget(title: 'School');
break;
default:
result = Text('Unknown');
break;
}
return result;
}
}
Edit: For your example, you may want to fetch some data from the network and only use the widget to display it. In that case, create a new class (not a Widget) to fetch & hold on to the data, and use the Widget only for displaying the data.
Some sample code:
/// Silly class to fetch data
class DataClass {
static int _nextDatum = 0;
int _data;
DataClass();
Future<int> fetchData() async {
await Future.delayed(Duration(
milliseconds: 2000,
));
_data = _nextDatum++;
return _data;
}
int getData() {
return _data;
}
}
class BaseClass extends StatefulWidget {
final String title;
final DataClass data;
const BaseClass({Key key, this.title, this.data}) : super(key: key);
#override
_BaseClassState createState() => _BaseClassState();
}
class _BaseClassState extends State<BaseClass> {
String title;
DataClass data;
#override
Widget build(BuildContext context) {
String dataStr = data == null ? ' - ' : '${data.getData()}';
return Center(
child: Text(
'$title: $dataStr',
),
);
}
#override
void initState() {
super.initState();
// initState gets called only ONCE
title = widget.title;
data = widget.data;
}
#override
void didUpdateWidget(BaseClass oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.data != oldWidget.data) {
data = widget.data;
}
if (widget.title != oldWidget.title) {
title = widget.title;
}
}
}
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int pageIndex = 0;
Map<String, DataClass> _dataMap = <String, DataClass>{};
#override
void initState() {
super.initState();
_init().then((result) {
// Since we need to rebuild the widget with the resulting data,
// make sure to use `setState`
setState(() {
_dataMap = result;
});
});
}
Future<Map<String, DataClass>> _init() async {
// this fetches the data only once
return <String, DataClass>{
'home': DataClass()..fetchData(),
'business': DataClass()..fetchData(),
'school': DataClass()..fetchData(),
};
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: choosePager(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('Business'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('School'),
),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
pageIndex = index;
});
}
Widget choosePager() {
Widget result;
switch (pageIndex) {
// it doesn't matter if you create a new BaseClass() a hundred times, flutter is optimized enough to not care. The `initState()` gets called only once. You're fetching the data only once.
case 0:
result = BaseClass(
title: 'Home',
data: _dataMap['home'],
);
break;
case 1:
result = BaseClass(
title: 'Business',
data: _dataMap['business'],
);
break;
case 2:
result = BaseClass(
title: 'School',
data: _dataMap['school'],
);
break;
default:
result = Text('Unknown');
break;
}
return result;
}
}
After lot of RND I solved my problem using IndexedStack. It shows the single Child from the list of children based on the index.
It will initialise all the children when the Widget build(BuildContext context) method of the _HomeWidgetState is called. So any time you switch the tabs, the object won't be reinitialised.
Here is my full code
class BaseWidget extends StatefulWidget {
final String title;
BaseWidget(this.title);
_BaseWidgetState createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
#override
Widget build(BuildContext context) {
return Center(child: Text(widget.title));
}
}
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int _pageIndex = 0;
List<Widget> _children;
#override
void initState() {
super.initState();
_children = [
BaseWidget('Home'),
BaseWidget('Business'),
BaseWidget('School')
];
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: IndexedStack(
children: _children,
index: _pageIndex,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(
icon: Icon(Icons.business), title: Text('Business')),
BottomNavigationBarItem(
icon: Icon(Icons.school), title: Text('School')),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
_pageIndex = index;
});
}
}
Please try this code, I have edited the BaseWidget class
class BaseWidget extends StatefulWidget {
final String title;
BaseWidget(this.title);
_BaseWidgetState createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
#override
Widget build(BuildContext context) {
return Center(child: Text(widget.title));
}
}
I have this code that has the parent widget Homepage and the child widget CountryList. In CountryList, I have created a function that uses an API to get a list of countries. I felt like enabling a RefreshIndicator in the app, so I had to modify the Homepage widget and add GlobalKey to access getCountryData() function of CountryList widget. The RefreshIndicator has done its job well. But the problem now is that when I pull and use the RefreshIndicator in the app, the getCountryData() function is called, but even after showing all data in the list, the circular spinner doesn't go (shown in the screenshot).
So, could anyone please suggest me a way to make the spinner go?
The code of main.dart containing Homepage widget is given below:
import 'package:flutter/material.dart';
import 'country_list.dart';
GlobalKey<dynamic> globalKey = GlobalKey();
void main() => runApp(MaterialApp(home: Homepage()));
class Homepage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("List of countries"), actions: <Widget>[
IconButton(icon: Icon(Icons.favorite), onPressed: (){},)
],),
body: RefreshIndicator(child: CountryList(key:globalKey), onRefresh: (){globalKey.currentState.getCountryData();},),
);
}
}
And the code of country_list.dart containing CountryList widget is:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter_svg/flutter_svg.dart';
class CountryList extends StatefulWidget {
CountryList({Key key}) : super(key: key);
#override
_CountryListState createState() => _CountryListState();
}
class _CountryListState extends State<CountryList> {
List<dynamic> _countryData;
bool _loading = false;
#override
void initState() {
// TODO: implement initState
super.initState();
this.getCountryData();
}
Future<String> getCountryData() async {
setState(() {
_loading = true;
});
var response =
await http.get(Uri.encodeFull("https://restcountries.eu/rest/v2/all"));
var decodedResponse = json.decode(response.body);
setState(() {
_countryData = decodedResponse;
_loading = false;
});
}
#override
Widget build(BuildContext context) {
return _loading?Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[CircularProgressIndicator(), Padding(padding: EdgeInsets.all(5.0),), Text("Loading data...", style: TextStyle(fontSize: 20.0),)],)):ListView.builder(
itemCount: _countryData.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: ListTile(
leading: SvgPicture.network(_countryData[index]['flag'], width: 60.0,),
title: Text(_countryData[index]['name']),
trailing: IconButton(
icon: Icon(Icons.favorite_border),
onPressed: () {},
),
),
);
},
);
}
}
You need to add return here:
Future<String> getCountryData() async {
setState(() {
_loading = true;
});
var response =
await http.get(Uri.encodeFull("https://restcountries.eu/rest/v2/all"));
var decodedResponse = json.decode(response.body);
setState(() {
_countryData = decodedResponse;
_loading = false;
});
return 'success';
}
and here:
body: RefreshIndicator(
child: CountryList(key: globalKey),
onRefresh: () {
return globalKey.currentState.getCountryData();
},
),
The onRefresh callback is called. The callback is expected to update the scrollable's contents and then complete the Future it returns. The refresh indicator disappears after the callback's Future has completed, I think you should return Future<String> from getCountryData.
I'm currently working on building a Flutter app that will preserve states when navigating from one screen, to another, and back again when utilizing BottomNavigationBar. Just like it works in the Spotify mobile application; if you have navigated down to a certain level in the navigation hierarchy on one of the main screens, changing screen via the bottom navigation bar, and later changing back to the old screen, will preserve where the user were in that hierarchy, including preservation of the state.
I have run my head against the wall, trying various different things without success.
I want to know how I can prevent the pages in pageChooser(), when toggled once the user taps the BottomNavigationBar item, from rebuilding themselves, and instead preserve the state they already found themselves in (the pages are all stateful Widgets).
import 'package:flutter/material.dart';
import './page_plan.dart';
import './page_profile.dart';
import './page_startup_namer.dart';
void main() => runApp(new Recipher());
class Recipher extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Pages();
}
}
class Pages extends StatefulWidget {
#override
createState() => new PagesState();
}
class PagesState extends State<Pages> {
int pageIndex = 0;
pageChooser() {
switch (this.pageIndex) {
case 0:
return new ProfilePage();
break;
case 1:
return new PlanPage();
break;
case 2:
return new StartUpNamerPage();
break;
default:
return new Container(
child: new Center(
child: new Text(
'No page found by page chooser.',
style: new TextStyle(fontSize: 30.0)
)
),
);
}
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
body: pageChooser(),
bottomNavigationBar: new BottomNavigationBar(
currentIndex: pageIndex,
onTap: (int tappedIndex) { //Toggle pageChooser and rebuild state with the index that was tapped in bottom navbar
setState(
(){ this.pageIndex = tappedIndex; }
);
},
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
title: new Text('Profile'),
icon: new Icon(Icons.account_box)
),
new BottomNavigationBarItem(
title: new Text('Plan'),
icon: new Icon(Icons.calendar_today)
),
new BottomNavigationBarItem(
title: new Text('Startup'),
icon: new Icon(Icons.alarm_on)
)
],
)
)
);
}
}
For keeping state in BottomNavigationBar, you can use IndexedStack
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
current_tab = index;
});
},
currentIndex: current_tab,
items: [
BottomNavigationBarItem(
...
),
BottomNavigationBarItem(
...
),
],
),
body: IndexedStack(
children: <Widget>[
PageOne(),
PageTwo(),
],
index: current_tab,
),
);
}
Late to the party, but I've got a simple solution. Use the PageView widget with the AutomaticKeepAliveClinetMixin.
The beauty of it that it doesn't load any tab until you click on it.
The page that includes the BottomNavigationBar:
var _selectedPageIndex;
List<Widget> _pages;
PageController _pageController;
#override
void initState() {
super.initState();
_selectedPageIndex = 0;
_pages = [
//The individual tabs.
];
_pageController = PageController(initialPage: _selectedPageIndex);
}
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
...
body: PageView(
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
...
currentIndex: _selectedPageIndex,
onTap: (selectedPageIndex) {
setState(() {
_selectedPageIndex = selectedPageIndex;
_pageController.jumpToPage(selectedPageIndex);
});
},
...
}
The individual tab:
class _HomeState extends State<Home> with AutomaticKeepAliveClientMixin<Home> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
//Notice the super-call here.
super.build(context);
...
}
}
I've made a video about it here.
Use AutomaticKeepAliveClientMixin to force your tab content to not be disposed.
class PersistantTab extends StatefulWidget {
#override
_PersistantTabState createState() => _PersistantTabState();
}
class _PersistantTabState extends State<PersistantTab> with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
return Container();
}
// Setting to true will force the tab to never be disposed. This could be dangerous.
#override
bool get wantKeepAlive => true;
}
To make sure your tab does get disposed when it doesn't require to be persisted, make wantKeepAlive return a class variable. You must call updateKeepAlive() to update the keep alive status.
Example with dynamic keep alive:
// class PersistantTab extends StatefulWidget ...
class _PersistantTabState extends State<PersistantTab>
with AutomaticKeepAliveClientMixin {
bool keepAlive = false;
#override
void initState() {
doAsyncStuff();
}
Future doAsyncStuff() async {
keepAlive = true;
updateKeepAlive();
// Keeping alive...
await Future.delayed(Duration(seconds: 10));
keepAlive = false;
updateKeepAlive();
// Can be disposed whenever now.
}
#override
bool get wantKeepAlive => keepAlive;
#override
Widget build(BuildContext context) {
super.build();
return Container();
}
}
Instead of returning new instance every time you run pageChooser, have one instance created and return the same.
Example:
class Pages extends StatefulWidget {
#override
createState() => new PagesState();
}
class PagesState extends State<Pages> {
int pageIndex = 0;
// Create all the pages once and return same instance when required
final ProfilePage _profilePage = new ProfilePage();
final PlanPage _planPage = new PlanPage();
final StartUpNamerPage _startUpNamerPage = new StartUpNamerPage();
Widget pageChooser() {
switch (this.pageIndex) {
case 0:
return _profilePage;
break;
case 1:
return _planPage;
break;
case 2:
return _startUpNamerPage;
break;
default:
return new Container(
child: new Center(
child: new Text(
'No page found by page chooser.',
style: new TextStyle(fontSize: 30.0)
)
),
);
}
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
body: pageChooser(),
bottomNavigationBar: new BottomNavigationBar(
currentIndex: pageIndex,
onTap: (int tappedIndex) { //Toggle pageChooser and rebuild state with the index that was tapped in bottom navbar
setState(
(){ this.pageIndex = tappedIndex; }
);
},
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
title: new Text('Profile'),
icon: new Icon(Icons.account_box)
),
new BottomNavigationBarItem(
title: new Text('Plan'),
icon: new Icon(Icons.calendar_today)
),
new BottomNavigationBarItem(
title: new Text('Startup'),
icon: new Icon(Icons.alarm_on)
)
],
)
)
);
}
}
Or you can make use of widgets like PageView or Stack to achieve the same.
Hope that helps!
Use “IndexedStack Widget” with “Bottom Navigation Bar Widget” to keep state of Screens/pages/Widget
Provide list of Widget to IndexedStack and index of widget you want to show because IndexedStack show single widget from list at one time.
final List<Widget> _children = [
FirstClass(),
SecondClass()
];
Scaffold(
body: IndexedStack(
index: _selectedPage,
children: _children,
),
bottomNavigationBar: BottomNavigationBar(
........
........
),
);
The most convenient way I have found to do so is using PageStorage widget along with PageStorageBucket, which acts as a key value persistent layer.
Go through this article for a beautiful explanation -> https://steemit.com/utopian-io/#tensor/persisting-user-interface-state-and-building-bottom-navigation-bars-in-dart-s-flutter-framework
Do not use IndexStack Widget, because it will instantiate all the tabs together, and suppose if all the tabs are making a network request then the callbacks will be messed up the last API calling tab will probably have the control of the callback.
Use AutomaticKeepAliveClientMixin for your stateful widget it is the simplest way to achieve it without instantiating all the tabs together.
My code had interfaces that were providing the respective responses to the calling tab I implemented it the following way.
Create your stateful widget
class FollowUpsScreen extends StatefulWidget {
FollowUpsScreen();
#override
State<StatefulWidget> createState() {
return FollowUpsScreenState();
}
}
class FollowUpsScreenState extends State<FollowUpsScreen>
with AutomaticKeepAliveClientMixin<FollowUpsScreen>
implements OperationalControls {
#override
Widget build(BuildContext context) {
//do not miss this line
super.build(context);
return .....;
}
#override
bool get wantKeepAlive => true;
}
This solution is based on CupertinoTabScaffold's implementation which won't load screens unnecessary.
import 'package:flutter/material.dart';
enum MainPage { home, profile }
class BottomNavScreen extends StatefulWidget {
const BottomNavScreen({super.key});
#override
State<BottomNavScreen> createState() => _BottomNavScreenState();
}
class _BottomNavScreenState extends State<BottomNavScreen> {
var currentPage = MainPage.home;
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageSwitchingView(
currentPageIndex: MainPage.values.indexOf(currentPage),
pageCount: MainPage.values.length,
pageBuilder: _pageBuilder,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: MainPage.values.indexOf(currentPage),
onTap: (index) => setState(() => currentPage = MainPage.values[index]),
items: const [
BottomNavigationBarItem(
label: 'Home',
icon: Icon(Icons.home),
),
BottomNavigationBarItem(
label: 'Profile',
icon: Icon(Icons.account_circle),
),
],
),
);
}
Widget _pageBuilder(BuildContext context, int index) {
final page = MainPage.values[index];
switch (page) {
case MainPage.home:
return ...
case MainPage.profile:
return ...
}
}
}
/// A widget laying out multiple pages with only one active page being built
/// at a time and on stage. Off stage pages' animations are stopped.
class PageSwitchingView extends StatefulWidget {
const PageSwitchingView({
super.key,
required this.currentPageIndex,
required this.pageCount,
required this.pageBuilder,
});
final int currentPageIndex;
final int pageCount;
final IndexedWidgetBuilder pageBuilder;
#override
State<PageSwitchingView> createState() => _PageSwitchingViewState();
}
class _PageSwitchingViewState extends State<PageSwitchingView> {
final List<bool> shouldBuildPage = <bool>[];
#override
void initState() {
super.initState();
shouldBuildPage.addAll(List<bool>.filled(widget.pageCount, false));
}
#override
void didUpdateWidget(PageSwitchingView oldWidget) {
super.didUpdateWidget(oldWidget);
// Only partially invalidate the pages cache to avoid breaking the current
// behavior. We assume that the only possible change is either:
// - new pages are appended to the page list, or
// - some trailing pages are removed.
// If the above assumption is not true, some pages may lose their state.
final lengthDiff = widget.pageCount - shouldBuildPage.length;
if (lengthDiff > 0) {
shouldBuildPage.addAll(List<bool>.filled(lengthDiff, false));
} else if (lengthDiff < 0) {
shouldBuildPage.removeRange(widget.pageCount, shouldBuildPage.length);
}
}
#override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: List<Widget>.generate(widget.pageCount, (int index) {
final active = index == widget.currentPageIndex;
shouldBuildPage[index] = active || shouldBuildPage[index];
return HeroMode(
enabled: active,
child: Offstage(
offstage: !active,
child: TickerMode(
enabled: active,
child: Builder(
builder: (BuildContext context) {
return shouldBuildPage[index] ? widget.pageBuilder(context, index) : Container();
},
),
),
),
);
}),
);
}
}
proper way of preserving tabs state in bottom nav bar is by wrapping the whole tree with PageStorage() widget which takes a PageStorageBucket bucket as a required named parameter and for those tabs to which you want to preserve its state pas those respected widgets with PageStorageKey(<str_key>) then you are done !! you can see more details in this ans which i've answered few weeks back on one question : https://stackoverflow.com/a/68620032/11974847
there's other alternatives like IndexedWidget() but you should beware while using it , i've explained y we should be catious while using IndexedWidget() in the given link answer
good luck mate ..