I have a Flutter application which comprises of a scaffold with a slider and a tabview.
The tabview comprises of a List which is show on each tab as seen in the picture below.
List<Widget> widgetList = <Widget>[
Post(),
Feed(),
Location(),
HomePage(),
Feed(),
];
Now I would like to refresh the current tab on screen when the slider is moved. However, since the classes are private i.e. _HomePageState, I do not know how to access the refreshList() method as shown in the snippet below.
homepage.dart:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
var list;
var random;
var refreshKey = GlobalKey<RefreshIndicatorState>();
#override
void initState() {
super.initState();
random = Random();
refreshList();
}
Future<Null> refreshList() async {
refreshKey.currentState?.show(atTop: false);
await Future.delayed(Duration(seconds: 2));
setState(() {
list = List.generate(random.nextInt(10), (i) => "Item $i");
});
return null;
}
}
The slider is not baked into each listview Widget i.e. homepage.dart as the slider values is applicable to each individual tab. How can I refresh the inner listview widget when the outer widget with the slider is moved?
There are different ways to deal with this:
You could pass down the PageController and the page index to your page widgets. In your page widgets can then listen to the changes (pageController.addListener(...)) and compare the currently centered page with their page index.
Create one ChangeNotifier for every page that you want to refresh:
final postRefresh = ChangeNotifier();
final feedRefresh = ChangeNotifier();
...
// build method of parent:
List<Widget> widgetList = <Widget>[
Post(refresh: postRefresh),
Feed(refresh: feedRefresh),
...
];
// initState method of parent:
pageController.addListener(() {
final roundedPage = pageController.page.round();
if(roundedPage == 0) {
postRefresh.notifyListeners();
}
else if(roundedPage == 1) {
feedRefresh.notifyListeners();
}
// ...
})
You could also give your page widgets a global key (for that, their State classes must be public):
// class body of parent:
final postPageKey = GlobalKey<PostPageState>();
final feedPageKey = GlobalKey<FeedPageState>();
// build method of parent:
List<Widget> widgetList = <Widget>[
Post(key: postPageKey),
Feed(key: feedPageKey),
...
];
// initState method of parent:
pageController.addListener(() {
final roundedPage = pageController.page.round();
if(roundedPage == 0) {
postPageKey.currentState?.refresh();
}
else if(roundedPage == 1) {
feedPageKey.currentState?.refresh();
}
// ...
})
Related
I've created my own simple bottom nav bar implementation in Flutter. When a tab is pressed, Flutter is currently re-creating the widget (initState() gets called every time) which is non-desirable.
I want the widgets to be persisted in memory so if they've already been created, they're simply popped straight in.
Main Widget
class _MainRootScreenState extends State<MainRootScreen> {
int _selectedIndex = 0;
List<Widget> _screens;
#override
void initState() {
// load pages
_screens = [
PageOne(),
PageTwo(),
PageThree()
];
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_selectedIndex],
bottomNavigationBar: _buildBottomTabBar(context)
);
}
}
so when _selectedIndex gets updated, the selected page is getting re-created.
I've tried using AutomaticKeepAliveClientMixin on the pages with no luck.
If you want that your widget/page should not rebuild when you click on tab button. You just need to follow this code
just add State<PageOne> with AutomaticKeepAliveClientMixin<PageOne> to your state class. after this you need to override a method called wantKeepAlive and make wantKeepAlive as true that's it.
By default wantKeepAlive is false because of it saves our memory .
PageOne
class PageOne extends StatefulWidget {
#override
_PageOneState createState() => _PageOneState();
}
class _PageOneState extends State<PageOne> with AutomaticKeepAliveClientMixin<PageOne> {
// Your code are here
#override
bool get wantKeepAlive => true;
}
Do the same from pageTwo and PageThree also that's it
Here is the summary of the code I'm having a problem with:
Parent widget
class HomePage extends StatefulWidget {
#override
State<HomePage> createState() => HomePageState();
}
class HomePageState extends State<HomePage> {
final GlobalKey<AsyncLoaderState> _asyncLoaderState = GlobalKey<AsyncLoaderState>();
List<DateTime> rounds;
List<PickupModel> pickups;
DateTime currentDate;
Widget build(BuildContext context) {
var _asyncLoader = AsyncLoader(
key: _asyncLoaderState,
initState: () async => await _getData(),
renderLoad: () => Scaffold(body: Center(child: CircularProgressIndicator())),
renderError: ([error]) => Text('Sorry, there was an error loading'),
renderSuccess: ({data}) => _buildScreen(context),
);
return _asyncLoader;
}
Widget _buildScreen(context) {
return Scaffold(
body: PickupList(pickups),
);
}
Future<Null> _selectDate(BuildContext context) async {
final DateTime picked = await showDatePicker(
context: context,
);
if (picked != null && picked != currentDate) {
currentDate = picked;
pickups = await api.fetchPickupList(currentDate);
setState(() {
});
}
}
_getData() async {
rounds = await api.fetchRoundsList();
currentDate = _getNextRound(rounds);
pickups = await api.fetchPickupList(currentDate);
}
}
Children Widget
(Listview builds tiles)
class PickupTile extends StatefulWidget{
final PickupModel pickup;
PickupTile(this.pickup);
#override
State<StatefulWidget> createState() {
return PickupTileState();
}
}
class PickupTileState extends State<PickupTile> {
Duration duration;
Timer timer;
bool _isUnavailable;
bool _isRunning = false;
bool _isDone = false;
#override
void initState() {
duration = widget.pickup.duration;
_isUnavailable = widget.pickup.customerUnavailable;
super.initState();
}
#override
Widget build(BuildContext context) {
return Row(
children: [
// UI widgets
]
}
So I have a parent widget an initial list of pickups which are displayed in the children PickupTile. One can change the date of the pickups displayed using _selectDate. This fetches a new list of Pickups which are stored in the parent State, and the children are rebuilt with their correct attributes. However, the State of the children widget (duration, isRunning, isDone...) is not reset so they stay on screen when changing the date.
If feel like I'm missing something obvious but I can't figure out how to reset the State of the children Widget or create new PickupTiles so that when changing the date I get new separate States.
I am trying to add a Image widget when I get the results of an API call. My code is:
class AnimalDetailsPage extends StatefulWidget {
final selection;
_AnimalDetailsPage createState() => new _AnimalDetailsPage();
AnimalDetailsPage({Key key, this.selection}) : super(key: key);
}
class _AnimalDetailsPage extends State<AnimalDetailsPage> {
Future<List> getphotos(horseId) async {
http.Response response = await http.get(
Uri.encodeFull(
"http://myhorses.com/api/getHorsePhotos?horse_id=" +
horseId));
return JSON.decode(response.body);
}
#override
Widget build(BuildContext context) {
final menu = new MyMenuBar();
List<Widget> bodyContent = [menu];
dynamic body = new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: bodyContent,
);
if (widget.selection != null) {
final horse = widget.selection;
getphotos(horse['id'].toString()).then((res) {
setState(() {
bodyContent.add(new Image.network(res[0]['image']));
});
});
}
return Scaffold(
body: body,
);
}
}
What I can't manage to understand is that the setState does not updates the view. If I move the setState out of the then statement and hard code the image src, then it works fine.
bodyContent is declared inside build()
#override
Widget build(BuildContext context) {
final menu = new MyMenuBar();
List<Widget> bodyContent = [menu];
...
bodyContent.add(new Image.network(res[0]['image']));
...
and setState() causes build to be executed again, which means the bodyContent that holds the image is discarded an a new one created.
Move List<Widget> bodyContent = [menu]; out of the build() method and make it a class-level field and you should get the desired result.
Material Design doesn't recommend sub-pages in the hierarchy to access the Bottom navigation bar. Hence, there's no clear way of implementing the same bottom navigator in all the screens uniformly. the issue is that once i move to sub pages, the same bottom navigator should be applicable to all
I'm not sure what you mean with
sub-pages in the hierarchy to access the Bottom navigation bar
But if you are trying to use the same BottomNavigationBar for multiple pages, try this:
import "package:flutter/material.dart";
void main() {
runApp(new MaterialApp(
home: new Example(),
));
}
class Example extends StatefulWidget {
#override
ExampleState createState() => new ExampleState();
}
class ExampleState extends State<Example> {
int currentTab = 0; // Index of currently opened tab.
PageOne pageOne = new PageOne(); // Page that corresponds with the first tab.
PageTwo pageTwo = new PageTwo(); // Page that corresponds with the second tab.
PageThree pageThree = new PageThree(); // Page that corresponds with the third tab.
List<Widget> pages; // List of all pages that can be opened from our BottomNavigationBar.
// Index 0 represents the page for the 0th tab, index 1 represents the page for the 1st tab etc...
Widget currentPage; // Page that is open at the moment.
#override
void initState() {
super.initState();
pages = [pageOne, pageTwo, pageThree]; // Populate our pages list.
currentPage = pageOne; // Setting the first page that we'd like to show our user.
// Notice that pageOne is the 0th item in the pages list. This corresponds with our initial currentTab value.
// These two should match at the start of our application.
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
// Here we create our BottomNavigationBar.
final BottomNavigationBar navBar = new BottomNavigationBar(
currentIndex: currentTab, // Our currentIndex will be the currentTab value. So we need to update this whenever we tab on a new page!
onTap: (int numTab) { // numTab will be the index of the tab that is pressed.
setState(() { // Setting the state so we can show our new page.
print("Current tab: " + numTab.toString()); // Printing for debugging reasons.
currentTab = numTab; // Updating our currentTab with the tab that is pressed [See 43].
currentPage = pages[numTab]; // Updating the page that we'd like to show to the user.
});
},
items: <BottomNavigationBarItem>[ // Visuals, see docs for more information: https://docs.flutter.io/flutter/material/BottomNavigationBar-class.html
new BottomNavigationBarItem( //numTab 0
icon: new Icon(Icons.ac_unit),
title: new Text("Ac unit")
),
new BottomNavigationBarItem( //numTab 1
icon: new Icon(Icons.access_alarm),
title: new Text("Access alarm")
),
new BottomNavigationBarItem( //numTab 2
icon: new Icon(Icons.access_alarms),
title: new Text("Access alarms")
)
],
);
return new Scaffold(
bottomNavigationBar: navBar, // Assigning our navBar to the Scaffold's bottomNavigationBar property.
body: currentPage, // The body will be the currentPage. Which we update when a tab is pressed.
);
}
}
class PageOne extends StatelessWidget { // Creating a simple example page.
#override
Widget build(BuildContext context) {
return new Center(child: new Text("Page one"));
}
}
class PageTwo extends StatelessWidget { // Creating a simple example page.
#override
Widget build(BuildContext context) {
return new Center(child: new Text("Page two"));
}
}
class PageThree extends StatelessWidget { // Creating a simple example page.
#override
Widget build(BuildContext context) {
return new Center(child: new Text("Page three"));
}
}
Please elaborate if this is not the wanted effect.
Despite flutter calling build (and printing the correct information as below), it doesn't seem to build new TaskWidgets (the print in TaskWidgetState's constructor is not called). This is creating some unusual behaviour in my application (for example, the persistence of deleted ListView items).
I have the following code:
class TaskWidget extends StatefulWidget {
TaskWidget({this.task, this.callToSave, this.callToDelete});
final Task task;
final Function callToSave;
final Function callToDelete;
#override
State<StatefulWidget> createState() {
return new TaskWidgetState(task, callToSave, callToDelete);
}
}
class TaskWidgetState extends State<TaskWidget>{
Task task;
Function toCallOnChange;
Function callToDelete;
TaskWidgetState(Task task, Function callToSave, Function callToDelete){
print("I'm a task widget for " + task.serialise().toString());
this.task = task;
toCallOnChange = callToSave;
this.callToDelete = callToDelete;
}
}
and
class ToDoListWidget extends State<ToDoList>{
List<Task> _toDo = new List<Task>();
...
#override
Widget build(BuildContext context) {
print("building");
return new Scaffold(
body: new ListView(
children: <Widget> [
generateCard(),
...
]
),
);
}
Widget generateCard() {
return new Card(
child: new Column (
children: generateWidgets()
),
...
);
}
List<Widget> generateWidgets() {
print("generating Widgets");
List<Task> tasks = getTasks();
List<Widget> widgets = new List<Widget>();
print("I have " + tasks.length.toString() + " widgets to build");
for(Task t in tasks) {
print(t.title);
TaskWidget widget = new TaskWidget(task: t, callToSave: saveList, callToDelete: deleteTask,);
widgets.add(widget);
}
return widgets;
}
}
Prints out:
building
I/flutter (28783): Returning for Daily
I/flutter (28783): // correct, undeleted task
but onscreen state doesn't reflect this
You're not using State and Stateful Widget properly.
How it works in flutter is that the Widget can be created many times, but there will most likely only be one instance of a State to go along with it.
It's a bit of an anti-pattern to have a constructor for a state.
Instead you should be doing something like this:
class TaskWidget extends StatefulWidget {
TaskWidget({this.task, this.callToSave, this.callToDelete});
final Task task;
final Function callToSave;
final Function callToDelete;
#override
State<StatefulWidget> createState() => new TaskWidgetState();
}
class TaskWidgetState extends State<TaskWidget>{
Widget build(Context context) {
// you can just use the widget.task, this is to illustrate.
var task = widget.task;
var callToSave = widget.callToSave;
var callToDelete = widget.calltoDelete;
}
}
This way, when the widget changes, your state will be re-built and will use whatever the updated values are that were passed into the widget.