Updating a Stateless Widget from a Stateful Widget - flutter

Main.dart:
import 'package:flutter/material.dart';
import 'package:mypackage/widgets/home_widget.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "My App",
theme: ThemeData(primarySwatch: Colors.blueGrey),
home: Home(),
);
}
}
Home.dart:
import 'package:flutter/material.dart';
import 'package:mypackage/widgets/secondary_page.dart';
import 'package:mypackage/widgets/home_view.dart';
import 'package:mypackage/widgets/settings_page.dart';
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomeState();
}
}
class _HomeState extends State<Home> {
int currentPageIndex = 0;
final List<Widget> pageWidgets = [HomeView(), SecondaryPage(), SettingsPage()];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My App"),
centerTitle: true,
),
body: pageWidgets[currentPageIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: onTabTapped,
currentIndex: currentPageIndex,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.home), label: "Tab One"),
BottomNavigationBarItem(
icon: new Icon(Icons.account_balance), label: "Tab Two"),
BottomNavigationBarItem(
icon: new Icon(Icons.settings), label: "Tab Three"),
],
),
floatingActionButton: FloatingActionButton(onPressed: () {
// Update data from here
HomeView().updateDisplay();
}));
}
// Changes the index when the tapped page has loaded
void onTabTapped(int index) {
setState(() {
currentPageIndex = index;
});
}
}
HomeView.dart:
import 'package:flutter/material.dart';
import 'package:mypackage/models/mydisplay.dart';
import 'package:mypackage/network/mydata.dart';
class HomeView extends StatelessWidget {
final List<MyDisplay> dataList = [
new MyDisplay("Sample Display",
"SampleFile", 0),
];
Future<void> updateDisplay() async {
MyData myData = new MyData();
dataList.forEach((d) async {
d.changePercentage = await myData
.getDataPercentage("assets/data/" + d.fileName + ".xml");
print(d.dataName +
" change percentage: " +
d.changePercentage.toString() +
"%");
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: new ListView.builder(
itemCount: dataList.length,
itemBuilder: (BuildContext context, int index) =>
buildCard(context, index)),
);
}
Widget buildCard(BuildContext context, int index) {
return Container(
child: Card(
color: Colors.teal.shade200,
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(children: <Widget>[
Text(dataList[index].dataName,
style: new TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
Text(dataList[index].changePercentage.toString() + "%")
]))));
}
}
When I call the updateDisplay() method from the floatingActionButton it works as expected, the data gets printed to the console, however it doesn't seem to update the actual UI, the content of the cards, they simply remain at their initial set value.
To clarify: how do I update the content of the cards inside the listview (inside a stateless widget) from a stateful widget?
I am sorry if this is a duplicate question, I have looked and am continuing looking for answers to the question but I simply can't get it working.

You need to use a stateful widget since you cannot update stateless widgets. Check this link out for more information.The next problem, calling it, can be fixed by using globalKeys. The only weird thing about this is you have to keep both the Home stateful widget and HomeView stateful widget in the same dart file. . Try this code to make it work.
HOME VIEW
import 'package:flutter/material.dart';
import 'package:mypackage/widgets/secondary_page.dart';
import 'package:mypackage/widgets/settings_page.dart';
import 'package:mypackage/models/mydisplay.dart';
import 'package:mypackage/network/mydata.dart';
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomeState();
}
}
class _HomeState extends State<Home> {
int currentPageIndex = 0;
final List<Widget> pageWidgets = [HomeView(), SecondaryPage(), SettingsPage()];
GlobalKey<_HomeViewState> _myKey = GlobalKey();// We declare a key here
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My App"),
centerTitle: true,
),
body: pageWidgets[currentPageIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: onTabTapped,
currentIndex: currentPageIndex,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.home), label: "Tab One"),
BottomNavigationBarItem(
icon: new Icon(Icons.account_balance), label: "Tab Two"),
BottomNavigationBarItem(
icon: new Icon(Icons.settings), label: "Tab Three"),
],
),
floatingActionButton: FloatingActionButton(onPressed: () {
// Update data from here
_myKey.currentState.updateDisplay();//This is how we call the function
}));
}
// Changes the index when the tapped page has loaded
void onTabTapped(int index) {
setState(() {
currentPageIndex = index;
});
}
}
class HomeView extends StatefulWidget {
Function updateDisplay;
HomeView({Key key}): super(key: key);//This key is what we use
#override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
final List<MyDisplay> dataList = [
new MyDisplay("Sample Display",
"SampleFile", 0),
];
#override
void initState(){
super.initState();
widget.updateDisplay = () async {
MyData myData = new MyData();
dataList.forEach((d) async {
d.changePercentage = await myData
.getDataPercentage("assets/data/" + d.fileName + ".xml");
print(d.dataName +
" change percentage: " +
d.changePercentage.toString() +
"%");
});
setState((){});//This line rebuilds the scaffold
};
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: new ListView.builder(
itemCount: dataList.length,
itemBuilder: (BuildContext context, int index) =>
buildCard(context, index)),
);
}
Widget buildCard(BuildContext context, int index) {
return Container(
child: Card(
color: Colors.teal.shade200,
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(children: <Widget>[
Text(dataList[index].dataName,
style: new TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
Text(dataList[index].changePercentage.toString() + "%")
]))));
}
}
We use setState((){}) to rebuild the widget. Just keep in mind one think. Wherever you use the updateDisplay method, dont make it so it runs in a loop after rebuilding. For eg. You use it in a future builder inside the child. THat way it will keep rebuilding
EDIT
We added a key to HomeView and now we can use that key like _myKey.currentState.yourFunction. Hope I helped

While I understand what you wish to achieve, I believe that will be going against "the Flutter way". You can achieve your desired result using a key, but I believe it will be easier and more idiomatic to lift the state up the widget tree. You can read about it in the docs here.
So in your case, you might change it to:
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomeState();
}
}
class _HomeState extends State<Home> {
int currentPageIndex = 0;
// Added this line
List<MyDisplay> dataList = [
new MyDisplay("Sample Display",
"SampleFile", 0),
];
final List<Widget> pageWidgets;
// moved the update function from HomeView to here
Future<void> updateDisplay() async {
MyData myData = new MyData();
dataList.forEach((d) async {
d.changePercentage = await myData
.getDataPercentage("assets/data/" + d.fileName + ".xml");
print(d.dataName +
" change percentage: " +
d.changePercentage.toString() +
"%");
});
setState((){});
}
#override
Widget build(BuildContext context) {
pageWidgets = [HomeView(dataList: dataList), SecondaryPage(), SettingsPage()]
return Scaffold(
appBar: AppBar(
title: Text("My App"),
centerTitle: true,
),
body: pageWidgets[currentPageIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: onTabTapped,
currentIndex: currentPageIndex,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.home), label: "Tab One"),
BottomNavigationBarItem(
icon: new Icon(Icons.account_balance), label: "Tab Two"),
BottomNavigationBarItem(
icon: new Icon(Icons.settings), label: "Tab Three"),
],
),
floatingActionButton: FloatingActionButton(onPressed: () {
updateDisplay();
}));
}
// Changes the index when the tapped page has loaded
void onTabTapped(int index) {
setState(() {
currentPageIndex = index;
});
}
}
Then for your HomeView widget:
class HomeView extends StatelessWidget {
HomeView({this.dataList});
final List<MyDisplay> dataList;
#override
Widget build(BuildContext context) {
return Scaffold(
body: new ListView.builder(
itemCount: dataList.length,
itemBuilder: (BuildContext context, int index) =>
buildCard(context, index)),
);
}
Widget buildCard(BuildContext context, int index) {
return Container(
child: Card(
color: Colors.teal.shade200,
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(children: <Widget>[
Text(dataList[index].dataName,
style: new TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
Text(dataList[index].changePercentage.toString() + "%")
],
),
),
),
);
}
}
I haven't tested if this works. I probably will a little later on.

You need to call setState(() {}); after HomeView().updateDisplay();

update your home.dart as follows.
import 'package:flutter/material.dart';
import 'package:mypackage/widgets/secondary_page.dart';
import 'package:mypackage/widgets/home_view.dart';
import 'package:mypackage/widgets/settings_page.dart';
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomeState();
}
}
class _HomeState extends State<Home> {
int currentPageIndex = 0;
Widget _homeView = HomeView();
final List<Widget> pageWidgets = [_homeView, SecondaryPage(), SettingsPage()];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My App"),
centerTitle: true,
),
body: pageWidgets[currentPageIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: onTabTapped,
currentIndex: currentPageIndex,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.home), label: "Tab One"),
BottomNavigationBarItem(
icon: new Icon(Icons.account_balance), label: "Tab Two"),
BottomNavigationBarItem(
icon: new Icon(Icons.settings), label: "Tab Three"),
],
),
floatingActionButton: FloatingActionButton(onPressed: () {
// Update data from here
_homeView.updateDisplay();
}));
}
// Changes the index when the tapped page has loaded
void onTabTapped(int index) {
setState(() {
currentPageIndex = index;
});
}
}
home_view.dart as follows
import 'package:flutter/material.dart';
import 'package:mypackage/models/mydisplay.dart';
import 'package:mypackage/network/mydata.dart';
class HomeView extends StatelessWidget {
final List<MyDisplay> dataList = [
new MyDisplay("Sample Display",
"SampleFile", 0),
];
Future<void> updateDisplay() async {
MyData myData = new MyData();
dataList.forEach((d) async {
d.changePercentage = await myData
.getDataPercentage("assets/data/" + d.fileName + ".xml");
print(d.dataName +
" change percentage: " +
d.changePercentage.toString() +
"%");
});
setState((){});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: new ListView.builder(
itemCount: dataList.length,
itemBuilder: (BuildContext context, int index) =>
buildCard(context, index)),
);
}
Widget buildCard(BuildContext context, int index) {
return Container(
child: Card(
color: Colors.teal.shade200,
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(children: <Widget>[
Text(dataList[index].dataName,
style: new TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
Text(dataList[index].changePercentage.toString() + "%")
]))));
}
}

You need to transform your HomeView into a StatefulWidget.
After that you need to modify updateDisplay into this:
Future<void> updateDisplay() async {
MyData myData = new MyData();
dataList.forEach((d) async {
d.changePercentage = await myData
.getDataPercentage("assets/data/" + d.fileName + ".xml");
print(d.dataName +
" change percentage: " +
d.changePercentage.toString() +
"%");
});
setState(() {});
}
By calling setState (() {}); you invoke the build method inside the StatefulWidget.
EDIT
Change your HomeView like this:
class HomeView extends StatefulWidget {
final List<MyDisplay> dataList = [
new MyDisplay("Sample Display",
"SampleFile", 0),
];
Future<void> updateDisplay() async {
MyData myData = new MyData();
dataList.forEach((d) async {
d.changePercentage = await myData
.getDataPercentage("assets/data/" + d.fileName + ".xml");
print(d.dataName +
" change percentage: " +
d.changePercentage.toString() +
"%");
});
}
#override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: new ListView.builder(
itemCount: widget.dataList.length,
itemBuilder: (BuildContext context, int index) =>
buildCard(context, index)),
);
}
Widget buildCard(BuildContext context, int index) {
return Container(
child: Card(
color: Colors.teal.shade200,
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(children: <Widget>[
Text(widget.dataList[index].dataName,
style: new TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
Text(widget.dataList[index].changePercentage.toString() + "%")
]))));
}
}
And your Home like this:
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomeState();
}
}
class _HomeState extends State<Home> {
int currentPageIndex = 0;
HomeView _homeView = HomeView();
List<Widget> pageWidgets = [_homeView, SecondaryPage(), SettingsPage()];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My App"),
centerTitle: true,
),
body: pageWidgets[currentPageIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: onTabTapped,
currentIndex: currentPageIndex,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.home), label: "Tab One"),
BottomNavigationBarItem(
icon: new Icon(Icons.account_balance), label: "Tab Two"),
BottomNavigationBarItem(
icon: new Icon(Icons.settings), label: "Tab Three"),
],
),
floatingActionButton: FloatingActionButton(onPressed: () {
// Update data from here
_homeView.updateDisplay();
}));
}
// Changes the index when the tapped page has loaded
void onTabTapped(int index) {
setState(() {
currentPageIndex = index;
});
}
}

Related

Flutter bottomNavigationBar not changing pages (when it's the "same page")

I have a bottom navigation bar and realized that the different pages/widgets that the navigator was going to were pretty much the exact same page (except for 2 parameters that changed). So instead of creating 2 pages/widgets which were pretty much identical (with only 2 differing parameters), I wanted to consolidate it into only one widget and pass the parameters from the page with the bottom navigator. The problem is that now that I did that it won't change the page it displays, or at least it won't change consistently (it usually will only show the page that corresponds to the first tab in the navigator (i.e., index = 0)). Here is my page with the bottom navigator:
class FreestylePage extends StatefulWidget {
const FreestylePage({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _FreestylePageState();
}
}
class _FreestylePageState extends State<FreestylePage> {
int _currentIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: showCategory(_currentIndex),
)),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.looks_one_outlined),
label: 'Single rope',
backgroundColor: Color.fromRGBO(204, 16, 138, 1)),
BottomNavigationBarItem(
icon: Icon(Icons.looks_two_outlined),
label: 'Double dutch',
backgroundColor: Color.fromRGBO(204, 16, 138, 1)),
],
onTap: (index) {
if (mounted) {
setState(() {
_currentIndex = index;
});
}
},
),
);
}
showCategory(index) {
if (index == 0) {
return [
WorkoutListPage(categoryIndex: 2, subCategories: Utils.srfDropdown)
];
} else {
return [
WorkoutListPage(categoryIndex: 3, subCategories: Utils.ddfDropdown)
];
}
}
}
And the WorkoutListPage looks as follows:
class WorkoutListPage extends StatefulWidget {
final int categoryIndex;
final List<String> subCategories;
const WorkoutListPage(
{Key? key, required this.categoryIndex, required this.subCategories})
: super(key: key);
#override
State<StatefulWidget> createState() {
return _WorkoutListPageState();
}
}
class _WorkoutListPageState extends State<WorkoutListPage> {
bool isLoading = true;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) =>
FutureBuilder<List<Map<String, dynamic>>>(
future: MyCard.getData(widget.categoryIndex, widget.subCategories)!
.whenComplete(() => setState(() {
isLoading = false;
})),
builder: ((context, snapshot) {
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
return FutureBuilder<List<MyCard>>(
future: MyCard.readData(snapshot.data),
builder: (context, cards) {
if (cards.hasData) {
final card = cards.data!;
return Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: card.length,
itemBuilder: (context, index) {
return MyCard.buildCard(card[index], context);
},
),
);
} else {
return const Text("No data");
}
});
} else {
return isLoading
? Column(
children: const [CircularProgressIndicator()],
)
: const Text("You do not have any workouts yet");
}
}),
);
}
This doesn't work, but ironically if I change my showCategory function in the widget with the bottom navigation bar to the following:
showCategory(index) {
if (index == 0) {
return [
WorkoutListPage(categoryIndex: 2, subCategories: Utils.srfDropdown)
];
} else {
return [const FreestyleDDPage()];
}
}
where the FreestyleDDPage is the following:
class FreestyleDDPage extends StatefulWidget {
const FreestyleDDPage({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _FreestyleDDPageState();
}
}
class _FreestyleDDPageState extends State<FreestyleDDPage> {
var isLoading = true;
#override
Widget build(BuildContext context) =>
FutureBuilder<List<Map<String, dynamic>>>(
future: MyCard.getData(3, Utils.ddfDropdown)!
.whenComplete(() => setState(() {
isLoading = false;
})),
builder: ((context, snapshot) {
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
return FutureBuilder<List<MyCard>>(
future: MyCard.readData(snapshot.data),
builder: (context, cards) {
if (cards.hasData) {
final card = cards.data!;
return Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: card.length,
itemBuilder: (context, index) {
return MyCard.buildCard(card[index], context);
},
),
);
} else {
return const Text("No data");
}
});
} else {
return isLoading
? Column(
children: const [CircularProgressIndicator()],
)
: const Text("You do not have any workouts yet");
}
}),
);
}
then it works.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _selectedIndex = 0;
static const TextStyle optionStyle =
TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
static const List<Widget> _widgetOptions = <Widget>[
CustomWidgetWithParametr(index: 0 , categoryName: "HOME"),
CustomWidgetWithParametr(index: 1 , categoryName: "BUSINES"),
CustomWidgetWithParametr(index: 2 , categoryName: "SCHOOL"),
CustomWidgetWithParametr(index: 3 , categoryName: "Settings"),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('BottomNavigationBar Sample'),
),
body: Center(
child: _widgetOptions.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
backgroundColor: Colors.red,
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Business',
backgroundColor: Colors.green,
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'School',
backgroundColor: Colors.purple,
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
backgroundColor: Colors.pink,
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
);
}
}
class CustomWidgetWithParametr extends StatefulWidget {
const CustomWidgetWithParametr({Key? key, required this.index, required this.categoryName}) : super(key: key);
final int index;
final String categoryName;
#override
State<CustomWidgetWithParametr> createState() => _CustomWidgetWithParametrState();
}
class _CustomWidgetWithParametrState extends State<CustomWidgetWithParametr> {
#override
Widget build(BuildContext context) {
return
Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(widget.index.toString()),
Text(widget.categoryName),
],
);
}
}

Flutter add a card widget on another screen on button tap

I am working on an app in which I want to add some card widgets on the home screen when a user selects the cards on the explore screen. Since I am new to flutter I need some assistance with code.
It is like it would be adding a favourite or bookmark feature.
If you want to check the full code here is the link to the github : https://github.com/Ankitkj1999/study_lpu/tree/mark_feature
explore.dart
import 'package:flutter/material.dart';
class ExploreScreen extends StatelessWidget {
const ExploreScreen({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return ListViewHome();
}
}
class ListViewHome extends StatelessWidget {
final titles = ["List 1", "List 2", "List 3", "List 4"];
final subtitles = [
"Here is list 1 subtitle",
"Here is list 2 subtitle",
"Here is list 3 subtitle",
"Here is list 4 subtitle"
];
// final icons = [
// // IconButton(onPressed: () {}, icon: const Icon(Icons.add)),
// // Icons.access_alarm,
// // Icons.access_time,
// Icons.add
// ];
ListViewHome({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: titles.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text(titles[index]),
subtitle: Text(subtitles[index]),
leading: const CircleAvatar(
backgroundImage: NetworkImage(
"https://images.unsplash.com/photo-1547721064-da6cfb341d50")),
trailing: IconButton(
onPressed: () {},
icon: const Icon(Icons.add),
),
),
);
});
}
}
class pushAdd extends StatelessWidget {
const pushAdd({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
showWidget() {
return const Scaffold(
body: Center(
child: Text('Home'),
),
);
}
return showWidget();
}
}
home.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:study_lpu/explore.dart';
import 'package:study_lpu/fav.dart';
import 'login.dart';
import 'explore.dart';
// bottom navigation tutorial https://blog.logrocket.com/how-to-build-a-bottom-navigation-bar-in-flutter/
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
late String uid;
int _selectedIndex = 0;
static const List<Widget> _pages = <Widget>[
// Scaffold(
// body: Center(
// child: Text('Hot'),
// ),
// ),
pushAdd(),
ExploreScreen(),
RandWords(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () async {
await FirebaseAuth.instance.signOut();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => const LoginScreen()),
(route) => false);
},
)
],
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.book_outlined),
label: 'Explore',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outlined),
label: 'Profile',
)
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
body:
// Center(
// // child: Text(uid),
// child: _pages.elementAt(_selectedIndex),
// ),
IndexedStack(
index: _selectedIndex,
children: _pages,
));
}
#override
void initState() {
// TODO: implement initState
super.initState();
uid = FirebaseAuth.instance.currentUser!.uid;
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
}
Here is how you can do that in a basic way without using any provider. Firstly you need to put your data in a model to access the variable properly. second, in home page I defined a list of model that is empty and also defined a function that adds card to the list executed in explore page. Also you need to make a view in home page to show added card to the list
class MyCardModel {
String? title;
String? subTitle;
String? leadingImage;
MyCardModel(String title, String subTitle, String leadingImage) {
this.title = title;
this.subTitle = subTitle;
this.leadingImage = leadingImage;
}
}
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
late String uid;
int _selectedIndex = 0;
List<MyCardModel> listOfCard = [];
void addCardToListMethod(MyCardModel card) {
setState(() {
listOfCard.add(card);
});
}
#override
Widget build(BuildContext context) {
List<Widget> _pages = <Widget>[
Scaffold(
body: Center(
child: ListView.builder(
itemCount: listOfCard.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text(listOfCard[index].title!),
subtitle: Text(listOfCard[index].subTitle!),
leading: CircleAvatar(
backgroundImage:
NetworkImage(listOfCard[index].leadingImage!)),
),
);
})),
),
pushAdd(),
ExploreScreen(addCardToListMethod: addCardToListMethod),
RandWords(),
];
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () async {
await FirebaseAuth.instance.signOut();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => const LoginScreen()),
(route) => false);
},
)
],
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.book_outlined),
label: 'Explore',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outlined),
label: 'Profile',
)
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
body:
// Center(
// // child: Text(uid),
// child: _pages.elementAt(_selectedIndex),
// ),
IndexedStack(
index: _selectedIndex,
children: _pages,
));
}
#override
void initState() {
// TODO: implement initState
super.initState();
uid = FirebaseAuth.instance.currentUser!.uid;
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
}
class ExploreScreen extends StatelessWidget {
Function? addCardToListMethod;
ExploreScreen({Key? key, this.addCardToListMethod}) : super(key: key);
List<MyCardModel> myCardList = [
MyCardModel("List1", "here is list 1 subtitle",
"https://images.unsplash.com/photo-1547721064-da6cfb341d50"),
MyCardModel("List1", "here is list 1 subtitle",
"https://images.unsplash.com/photo-1547721064-da6cfb341d50")
];
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: myCardList.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text(myCardList[index].title!),
subtitle: Text(myCardList[index].subTitle!),
leading: CircleAvatar(
backgroundImage:
NetworkImage(myCardList[index].leadingImage!)),
trailing: IconButton(
onPressed: () {
addCardToListMethod!(myCardList[index]);
},
icon: const Icon(Icons.add),
),
),
);
});
}
}

Flutter - Call a function defined in another dart file to refresh a ListView

I have 2 dart files home.dart and video_list.dart.
I have a PopupMenuButton and a BottomNavigationBar in home.dart. video_list.dart is shown in the "body:" of home.dart.
Each time the value changes in PopupMenuButton in home.dart, I want to call a function in video_list.dart (_loadPlaylist()) so that the ListView in video_list.dart is refreshed.
How is this possible? I am a newbie.
home.dart
import 'package:crt/models/subject.dart';
import 'package:crt/pages/faq.dart';
import 'package:crt/pages/quiz.dart';
import 'package:crt/pages/video_list.dart';
import 'package:crt/pages/resource_list.dart';
import 'package:flutter/material.dart';
import 'package:crt/data/subjects.dart';
class Home extends StatefulWidget {
Home({Key? key}) : super(key: key);
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
//GlobalKey<_VideoListState> _key = GlobalKey<_VideoListState>();
Subject _selectedSubject = subjects[0];
int _currentIndex = 0;
List<StatefulWidget> pages = [];
#override
void initState() {
super.initState();
_setPages();
}
void _setPages() {
pages = [
VideoList(_selectedSubject),
ResourceList(_selectedSubject),
Quiz(_selectedSubject),
Faq(_selectedSubject),
];
}
IndexedStack _loadPage() {
return IndexedStack(
index: _currentIndex,
children: pages,
);
}
void _changeSubject(Subject subject) {
setState(() {
_selectedSubject = subject;
_setPages();
if (_selectedSubject.name == 'English') {
// I want to call _loadPlaylist function defined in video_list.dart
// VideoList()._loadPlaylist;
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_selectedSubject.name),
elevation: 0,
actions: [
PopupMenuButton<Subject>(
onSelected: _changeSubject,
itemBuilder: (BuildContext context) {
return subjects.map((Subject subject) {
return PopupMenuItem<Subject>(
value: subject,
child: Text(subject.name),
);
}).toList();
},
)
],
),
body: _loadPage(),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
iconSize: 30,
selectedFontSize: 14,
unselectedFontSize: 14,
currentIndex: _currentIndex,
onTap: (index) => setState(() {
_currentIndex = index;
}),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.video_library),
label: 'Videos',
),
BottomNavigationBarItem(
icon: Icon(Icons.library_books),
label: 'Resources',
),
BottomNavigationBarItem(
icon: Icon(Icons.quiz),
label: 'Quiz',
),
BottomNavigationBarItem(
icon: Icon(Icons.question_answer),
label: 'FAQ',
),
],
),
);
}
}
video_list.dart
import 'package:crt/models/subject.dart';
import 'package:flutter/material.dart';
import 'package:crt/models/playlist.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:crt/utilities/services.dart';
class VideoList extends StatefulWidget {
VideoList(Subject this.selectedSubject, {Key? key}) : super(key: key);
final Subject selectedSubject;
#override
State<VideoList> createState() => _VideoListState();
}
class _VideoListState extends State<VideoList> {
late Playlist _playlist;
bool _isLoading = true;
late String _name;
#override
void initState() {
super.initState();
_playlist = Playlist();
_playlist.items = List.empty(growable: true);
_loadPlaylist();
}
//I want to call this function from home.dart
_loadPlaylist() async {
String playlistId = widget.selectedSubject.playlistId;
Playlist playlist = await Services.getPlaylist(playlistId: playlistId);
_playlist.items?.addAll(playlist.items as Iterable<Item>);
setState(() {
_isLoading = false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: ListView.builder(
itemCount: _playlist.items?.length,
itemBuilder: ((context, index) {
Item item = _playlist.items![index];
return Card(
child: InkWell(
onTap: () async {
// Navigator.push(context, MaterialPageRoute(builder: (context) {
// return VideoPlayer(
// item: item,
// );
// }));
},
child: Row(children: [
Padding(
padding: const EdgeInsets.all(5.0),
child: CachedNetworkImage(
imageUrl:
item.snippet.thumbnails.thumbnailsDefault.url),
),
SizedBox(
width: 10.0,
),
Flexible(
child: Text(
item.snippet.title,
style: TextStyle(
fontSize: 18.0,
),
)),
const SizedBox(
width: 20.0,
),
]),
),
);
})),
);
}
}
Just move the _loadPlayList() function to Home and pass it down to VideoList widget.
class VideoList extends StatefulWidget {
VideoList(Subject this.selectedSubject, {Key? key, this.loadPlayList})
: super(key: key);
final Subject selectedSubject;
final Future<Function> loadPlayList;
#override
State<VideoList> createState() => _VideoListState();
}
The whole code - home.dart
import 'package:crt/models/subject.dart';
import 'package:crt/pages/faq.dart';
import 'package:crt/pages/quiz.dart';
import 'package:crt/pages/video_list.dart';
import 'package:crt/pages/resource_list.dart';
import 'package:flutter/material.dart';
import 'package:crt/data/subjects.dart';
class Home extends StatefulWidget {
Home({Key? key}) : super(key: key);
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
//GlobalKey<_VideoListState> _key = GlobalKey<_VideoListState>();
Subject _selectedSubject = subjects[0];
int _currentIndex = 0;
List<StatefulWidget> pages = [];
#override
void initState() {
super.initState();
_setPages();
}
void _setPages() {
pages = [
VideoList(_selectedSubject, loadPlayList()),
ResourceList(_selectedSubject),
Quiz(_selectedSubject),
Faq(_selectedSubject),
];
}
loadPlaylist() async {
String playlistId = widget.selectedSubject.playlistId;
Playlist playlist = await Services.getPlaylist(playlistId: playlistId);
_playlist.items?.addAll(playlist.items as Iterable<Item>);
}
IndexedStack _loadPage() {
return IndexedStack(
index: _currentIndex,
children: pages,
);
}
void _changeSubject(Subject subject) {
setState(() {
_selectedSubject = subject;
_setPages();
if (_selectedSubject.name == 'English') {
this.loadPlaylist();
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_selectedSubject.name),
elevation: 0,
actions: [
PopupMenuButton<Subject>(
onSelected: _changeSubject,
itemBuilder: (BuildContext context) {
return subjects.map((Subject subject) {
return PopupMenuItem<Subject>(
value: subject,
child: Text(subject.name),
);
}).toList();
},
)
],
),
body: _loadPage(),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
iconSize: 30,
selectedFontSize: 14,
unselectedFontSize: 14,
currentIndex: _currentIndex,
onTap: (index) => setState(() {
_currentIndex = index;
}),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.video_library),
label: 'Videos',
),
BottomNavigationBarItem(
icon: Icon(Icons.library_books),
label: 'Resources',
),
BottomNavigationBarItem(
icon: Icon(Icons.quiz),
label: 'Quiz',
),
BottomNavigationBarItem(
icon: Icon(Icons.question_answer),
label: 'FAQ',
),
],
),
);
}
}
video_list.dart
import 'package:crt/models/subject.dart';
import 'package:flutter/material.dart';
import 'package:crt/models/playlist.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:crt/utilities/services.dart';
class VideoList extends StatefulWidget {
VideoList(Subject this.selectedSubject, this.loadPlayList, {Key? key})
: super(key: key);
final Subject selectedSubject;
final Future<Function> loadPlayList;
#override
State<VideoList> createState() => _VideoListState();
}
class _VideoListState extends State<VideoList> {
late Playlist _playlist;
bool _isLoading = true;
late String _name;
#override
void initState() {
super.initState();
_playlist = Playlist();
_playlist.items = List.empty(growable: true);
widget.loadPlaylist().then(() {
setState(() {
_isLoading = false;
});
};
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: ListView.builder(
itemCount: _playlist.items?.length,
itemBuilder: ((context, index) {
Item item = _playlist.items![index];
return Card(
child: InkWell(
onTap: () async {
// Navigator.push(context, MaterialPageRoute(builder: (context) {
// return VideoPlayer(
// item: item,
// );
// }));
},
child: Row(children: [
Padding(
padding: const EdgeInsets.all(5.0),
child: CachedNetworkImage(
imageUrl:
item.snippet.thumbnails.thumbnailsDefault.url),
),
SizedBox(
width: 10.0,
),
Flexible(
child: Text(
item.snippet.title,
style: TextStyle(
fontSize: 18.0,
),
)),
const SizedBox(
width: 20.0,
),
]),
),
);
})),
);
}
}

Flutter: Nested routing with persistent BottomNavigationBar but without building the unselected pages unnecessarily

Throughout the internet and stackoverflow I've searched and seen a lot of solutions to the problem of nested navigation with a persistent BottomNavigationBar for Flutter apps. Some of them using Navigators with IndexedStack or PageView and so on and so forth. All of them work just fine except that they will unnecessarily build the unselected tabs (sometimes even rebuilding all of them every time you switch tabs) thus making the solution not performatic. I did finally come up with a solution to that – as I was struggling with this problem myself.
The solution is very basic but hopefully you will be able to build upon it and adapt it. It achieves the following:
nests navigation while persisting the BottomNavigationBar
does not build a tab unless it has been selected
preserves the navigation state
preserves the scroll state (of a ListView, for example)
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: MyApp(),
));
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<Widget> _pages;
List<BottomNavigationBarItem> _items = [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: "Home",
),
BottomNavigationBarItem(
icon: Icon(Icons.messenger_rounded),
label: "Messages",
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: "Settings",
)
];
int _selectedPage;
#override
void initState() {
super.initState();
_selectedPage = 0;
_pages = [
MyPage(
1,
"Page 01",
MyKeys.getKeys().elementAt(0),
),
// This avoid the other pages to be built unnecessarily
SizedBox(),
SizedBox(),
];
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
onWillPop: () async {
return !await Navigator.maybePop(
MyKeys.getKeys()[_selectedPage].currentState.context,
);
},
child: IndexedStack(
index: _selectedPage,
children: _pages,
),
),
bottomNavigationBar: BottomNavigationBar(
items: _items,
currentIndex: _selectedPage,
onTap: (index) {
setState(() {
// now check if the chosen page has already been built
// if it hasn't, then it still is a SizedBox
if (_pages[index] is SizedBox) {
if (index == 1) {
_pages[index] = MyPage(
1,
"Page 02",
MyKeys.getKeys().elementAt(index),
);
} else {
_pages[index] = MyPage(
1,
"Page 03",
MyKeys.getKeys().elementAt(index),
);
}
}
_selectedPage = index;
});
},
),
);
}
}
class MyPage extends StatelessWidget {
MyPage(this.count, this.text, this.navigatorKey);
final count;
final text;
final navigatorKey;
#override
Widget build(BuildContext context) {
// You'll see that it will only print once
print("Building $text with count: $count");
return Navigator(
key: navigatorKey,
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(this.text),
),
body: Center(
child: RaisedButton(
child: Text(this.count.toString()),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (ctx) => MyCustomPage(count + 1, text)));
},
),
),
);
},
);
},
);
}
}
class MyCustomPage extends StatelessWidget {
MyCustomPage(this.count, this.text);
final count;
final text;
#override
Widget build(BuildContext parentContext) {
return Scaffold(
appBar: AppBar(
title: Text(this.text),
),
body: Column(
children: [
Expanded(
child: Container(
child: ListView.builder(
itemCount: 15,
itemBuilder: (context, index) {
return Container(
width: double.infinity,
child: Card(
child: Center(
child: RaisedButton(
child: Text(this.count.toString() + " pos($index)"),
onPressed: () {
Navigator.of(parentContext).push(MaterialPageRoute(
builder: (ctx) =>
MyCustomPage(count + 1, text)));
},
),
),
),
);
},
),
),
),
],
),
);
}
}
class MyKeys {
static final first = GlobalKey(debugLabel: 'page1');
static final second = GlobalKey(debugLabel: 'page2');
static final third = GlobalKey(debugLabel: 'page3');
static List<GlobalKey> getKeys() => [first, second, third];
}

Is there a way to display the BottomNavigationBar on every View?

I am trying to display the BottomNavigationBar on every View I have, it's working but this is a "dumb" way to do that...
I have a custom BottomNavigationBar which I am inserting in every View.
var selectedIndex = 0;
class CustomBottombar extends StatefulWidget {
CustomBottombar({Key key}) : super(key: key);
#override
_CustomBottombarState createState() => _CustomBottombarState();
}
class _CustomBottombarState extends State<CustomBottombar> {
List _viewList = [FirstView(), SecondView()];
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: selectedIndex,
onTap: _onItemTapped,
items: _items,
);
}
void _onItemTapped(int index) {
setState(() {
selectedIndex = index;
Navigator.of(context).popUntil((route) => route.isFirst
);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => _viewList[index]),
);
});
}
final _items = [
BottomNavigationBarItem(
icon: Icon(
Icons.refresh,
color: Color(0xFFACACAC),
size: 35,
),
title: Text("first")),
BottomNavigationBarItem(
icon: Icon(
Icons.phone,
color: Color(0xFFACACAC),
size: 35,
),
title: Text("second"),
),
BottomNavigationBarItem(
icon: Icon(
Icons.add_shopping_cart,
color: Color(0xFFACACAC),
size: 35,
),
title: Text("thrid"),
),
];
}
in the _onItemTapped function I pop everything from the "Navigationstack" and then I am displaying the Screen that is in my Items.
in my FirstView() I have then this code
class FirstView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(""),
bottomNavigationBar: CustomBottombar(),
endDrawer: CustomDrawer(),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ContactView()),
);
},
child: Text('First'),
),
),
);
}
}
Now I want to move to "ContactView" which is not an Item in the BottomNavigationBar
class ContactState extends State<ContactView> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar("title"),
endDrawer: CustomDrawer(),
bottomNavigationBar: CustomBottombar(),
body: SafeArea(
bottom: true,
child: SingleChildScrollView(
child: Container(child: Text("Contact"),),
)),
);
}
}
I'll also have a lot of other views which are not in the items array but I want to display the BottomNavigationBar on.
My Issue is really this function.
void _onItemTapped(int index) {
setState(() {
selectedIndex = index;
Navigator.of(context).popUntil((route) => route.isFirst
);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => _viewList[index]),
);
});
}
because here I'm deleting the navigation history to display the View which is in the Items Array.
Is there a standard way to do this, Hopefully, someone can help.
EDIT:
For clarification: I have like 10 Screens. Only 3 of those are navigatiable via BottomNavigationBar, Let's say the first 3 of those 10. now I want to Navigate to Screen4 from Screen1. The navigationbar disappears on screen4. I want Want to keep the Navigationbar on all Screens.
Edit 2
#Dhaval Kansara answer worked for me but I got a new Problem.
I have an enddrawer, before the fix it was above the BottomNavigationBar now the BottomNavigationBar is above.
but I want it like this
Use CupertinoTabBar as shown below for the static BottomNavigationBar.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mqttdemo/Screen2.dart';
import 'package:mqttdemo/Screen3.dart';
import 'Screen1.dart';
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int _currentIndex;
List<Widget> _children;
#override
void initState() {
_currentIndex = 0;
_children = [
Screen1(),
Screen2(),
Screen3(),
];
super.initState();
}
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
currentIndex: _currentIndex,
onTap: onTabTapped,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text("Screen 1"),
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text("Screen 2"),
),
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text("Screen 3")),
],
),
tabBuilder: (BuildContext context, int index) {
return CupertinoTabView(
builder: (BuildContext context) {
return SafeArea(
top: false,
bottom: false,
child: CupertinoApp(
home: CupertinoPageScaffold(
resizeToAvoidBottomInset: false,
child: _children[_currentIndex],
),
),
);
},
);
}
);
}
void onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
}
Navigate to screen4 from Screen3 as shown below:
class Screen3 extends StatefulWidget {
#override
_Screen3State createState() => _Screen3State();
}
class _Screen3State extends State<Screen3> {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Center(
child: RaisedButton(
child: Text("Click me"),
onPressed: () {
Navigator.of(context, rootNavigator: false).push(MaterialPageRoute(
builder: (context) => Screen4(), maintainState: false));
},
),
),
);
}
}