I am making a simple tab bar based app with Flutter. It uses CupertinoTabScaffold and CupertinoTabBar. Each CupertinoTabView has its own WebView.
In the first TabView, a button is placed that if clicked the selected tab will be changed to the next one to load a CupertinoTabView containing a WebView widget, then navigate to the certain website URL (note that the URL is not written on WebView:initialURL, but retrieved from the first tab).
The problem is that when clicking the button, while the selected tab is changed, WebView does not load a specific URL because the WebView controller is not assigned.
How to make the app 'wait' for the new CupertinoTabView widget to be fully loaded?
If I run the app, the error is the following:
Unhandled Exception: NoSuchMethodError: The method 'navigateTo' was called on null.
This seems because when clicking the button to execute the following code, before the line tabController.index = tabIndex will take effect, keyWebPage.curentState.navigateTo (which is null yet) is called.
tabController.index = tabIndex;
keyWebPage.currentState.navigateTo(url); //executed 'before' the new TabView initialization
I suspect the reason for this problem, but couldn't figure out how to solve it.
main.dart
final GlobalKey<PageWebState> keyWebPage = GlobalKey();
class _MyHomePageState extends State<MyHomePage> {
final CupertinoTabController tabController = CupertinoTabController();
int _currentTabIndex = 0;
WebPage pageWeb;
#override
void initState() {
super.initState();
pageWeb = new PageWeb(
this,
key: keyWebPage,
);
}
void navigateTab(int tabIndex, String url) {
setState(() {
_currentTabIndex = tabIndex;
tabController.index = tabIndex;
// HERE is the error occurring part
keyWebPage.currentState.navigateTo(url);
});
}
#override
Widget build(BuildContext context) {
final Widget scaffold = CupertinoTabScaffold(
controller: tabController,
tabBar: CupertinoTabBar(
currentIndex: _currentTabIndex,
onTap: (index) {
setState(() {
_currentTabIndex = index;
});
},
items: const <BottomNavigationBarItem>[
const BottomNavigationBarItem(icon: const Icon(Icons.home), title: Text('Start')),
const BottomNavigationBarItem(icon: const Icon(Icons.web), title: Text('Webpage')),
],
),
tabBuilder: (context, index) {
CupertinoTabView _viewWidget;
switch (index) {
case 0:
_viewWidget = CupertinoTabView(
builder: (context) {
return Center(
child: CupertinoButton(
child: Text('Navigate'),
onPressed: () {
navigateTab(1, 'https://stackoverflow.com/');
},
),
);
},
);
break;
case 1:
_viewWidget = CupertinoTabView(
builder: (context) {
return pageWeb;
},
);
break;
}
return _viewWidget;
},
);
return scaffold;
}
}
WebPage.dart
final Completer<WebViewController> _controller = Completer<WebViewController>();
class PageWeb extends StatefulWidget {
final delegate;
final ObjectKey key;
PageWeb(this.delegate, this.key);
#override
PageWebState createState() {
return PageWebState();
}
}
void navigateTo(String url) { //Function that will be called on main.dart
controller.future.then((controller) => controller.loadUrl(url));
}
class PageWebState extends State<PageWeb> {
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text("Test"),
),
child: SafeArea(
child: Container(
child: new WebView(
key: widget.key,
initialUrl: "https://www.google.com/",
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
_controller.complete(webViewController);
},
))));
}
}
Please note that using constructor to passing variables is not in consideration because the URL will be changed and the clicking the button can be more than once.
Related
I want to close my Drawer widget every time the user presses a button in the Bottom Navigation Bar, but can't quite figure this thing out. The way my setup of the BNB is set now is that the current state of all screen is remembered through out the app (using an IndexedStack), but I want to close the Drawer if it is opened in any of the screens before the BNB button press. Each of my screens have their own Drawers and AppBars, so I can't make one Drawer inside the BNB (or I can and I can dynamically change them with a switch case when a specific Screen is clicked on BUT then the Drawer will cover the Bottom Navigation Bar etc.), but I want to make it work like this for now. So here is the code with some comments inside to explain things:
Bottom Navigation Bar:
class BottomNavBar extends StatefulWidget {
static const String id = 'bottom_navbar_screen';
#override
_BottomNavBarState createState() => _BottomNavBarState();
}
class _BottomNavBarState extends State<BottomNavBar> {
int _selectedIndex = 0;
/// list of screen that will render inside the BNB
List<Navigation> _items = [
Navigation(
widget: Screen1(), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen2(), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen3(), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen4(), navigationKey: GlobalKey<NavigatorState>()),
];
/// function that renders components based on selected one in the BNB
void _onItemTapped(int index) {
if (index == _selectedIndex) {
_items[index]
.navigationKey
.currentState
.popUntil((route) => route.isFirst);
} else {
setState(() {
_selectedIndex = index;
});
}
/// when the index is selected, on the button press do some actions
switch (_selectedIndex) {
case 0:
// Do some actions
break;
case 1:
// Do some actions
break;
case 2:
// Do some actions
break;
case 3:
// Do some actions
break;
}
}
/// navigation Tab widget for a list of all the screens and puts them in a Indexed Stack
Widget _navigationTab(
{GlobalKey<NavigatorState> navigationKey, Widget widget}) {
return Navigator(
key: navigationKey,
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(builder: (context) => widget);
},
);
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final isFirstRouteInCurrentTab =
!await _items[_selectedIndex].navigationKey.currentState.maybePop();
if (isFirstRouteInCurrentTab) {
if (_selectedIndex != 0) {
_onItemTapped(1);
return false;
}
}
/// let system handle back button if we're on the first route
return isFirstRouteInCurrentTab;
},
child: Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _items
.map((e) => _navigationTab(
navigationKey: e.navigationKey, widget: e.widget))
.toList(),
),
bottomNavigationBar: BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: 'Screen 1,
),
BottomNavigationBarItem(
label: 'Screen 2,
),
BottomNavigationBarItem(
label: 'Screen 3,
),
BottomNavigationBarItem(
label: 'Screen 4,
),
],
currentIndex: _selectedIndex,
showUnselectedLabels: true,
onTap: _onItemTapped,
),
),
);
}
}
Let's say all 4 screens are the same and they have their own AppBar and Drawer:
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
drawer: Drawer(), // so this is what I want to close on BNB button press in each of the 4 screens
appBar: AppBar( // each screen has its own app bar
title: Text('Screens 1-4'),
),
body: Text('Body of Screens 1-4'),
);
}
So because each of the screens have their own AppBars and Drawers, the Drawer doesn't render over the Bottom Navigation Bar, so my BNB buttons can be clicked. If I put one Drawer for all Screens inside the BNB, then you can't click the BNB unless you close the Drawer first, which is not something I'm looking for right now.
So, my final question is, how do I close each of the Screens Drawers (if they are previously opened that is) when you press the BottomnavigationBar? (i.e. I am on Screen 1, I open the Drawer, then I press on Screen 2 in the BNB and I want to pop()/close the Drawer in Screen 1 before I navigate to Screen 2.)
Thanks in advance for your help!
A good way to do this is to use a GlobalKey for your scaffold.
So, for all your scaffolds, you define them using:
class SomeClass extends StatelessWidget {
final scaffoldKey = GlobalKey<ScaffoldState>()
Widget build(BuildContext context) {
Scaffold(
backgroundColor: Colors.white,
drawer: Drawer(), // so this is what I want to close on BNB button press in each of the 4 screens
appBar: AppBar( // each screen has its own app bar
title: Text('Screens 1-4),
),
body: Text('Body of Screens 1-4),
key: scaffoldKey,
),
);
}
}
And then, you can pass this key to your BottomNavigationBar.
In your BottomNavigationBar, you can have all the scaffoldKeys, and in the onItemTap function:
void _onItemTapped(int index) {
for (scaffoldKey in scaffoldKeys) {
// If the drawer is open
if (scaffoldKey.currentState.isDrawerOpen) {
// Closes the drawer
scaffoldKey.currentState?.openEndDrawer();
}
}
if (index == _selectedIndex) {
_items[index]
.navigationKey
.currentState
.popUntil((route) => route.isFirst);
} else {
setState(() {
_selectedIndex = index;
});
}
/// when the index is selected, on the button press do some actions
switch (_selectedIndex) {
case 0:
// Do some actions
break;
case 1:
// Do some actions
break;
case 2:
// Do some actions
break;
case 3:
// Do some actions
break;
}
}
It's up to you to find the best way of passing around the keys. You could for example define them in a Widgets that contains both the bottom navigation bar and the different scaffolds, and pass it down as parameters. You could use State Management... whatever fits your use case.
Here is what your code could look like:
class BottomNavBar extends StatefulWidget {
static const String id = 'bottom_navbar_screen';
#override
_BottomNavBarState createState() => _BottomNavBarState();
}
class _BottomNavBarState extends State<BottomNavBar> {
int _selectedIndex = 0;
late final List<GlobalKey<ScaffoldState>> scaffoldKeys;
/// list of screen that will render inside the BNB
late final List<Navigation> _items;
#override
initState() {
super.initState()
scaffoldKeys = [GlobalKey<ScaffoldState>(), GlobalKey<ScaffoldState>(), GlobalKey<ScaffoldState>(), GlobalKey<ScaffoldState>()];
_items = [
Navigation(
widget: Screen1(scaffoldKey: scaffoldKeys[0]), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen2(scaffoldKey: scaffoldKeys[1]), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen3(scaffoldKey: scaffoldKeys[2]), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen4(scaffoldKey: scaffoldKeys[3]), navigationKey: GlobalKey<NavigatorState>()),
];
}
/// function that renders components based on selected one in the BNB
void _onItemTapped(int index) {
for (scaffoldKey in scaffoldKeys) {
// If the drawer is open
if (scaffoldKey.currentState.isDrawerOpen) {
// Closes the drawer
scaffoldKey.currentState?.openEndDrawer();
}
}
if (index == _selectedIndex) {
_items[index]
.navigationKey
.currentState
.popUntil((route) => route.isFirst);
} else {
setState(() {
_selectedIndex = index;
});
}
/// when the index is selected, on the button press do some actions
switch (_selectedIndex) {
case 0:
// Do some actions
break;
case 1:
// Do some actions
break;
case 2:
// Do some actions
break;
case 3:
// Do some actions
break;
}
}
/// navigation Tab widget for a list of all the screens and puts them in a Indexed Stack
Widget _navigationTab(
{GlobalKey<NavigatorState> navigationKey, Widget widget, GlobalKey<ScaffoldState> scaffoldKey}) {
return Navigator(
key: navigationKey,
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(builder: (context) => widget);
},
);
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final isFirstRouteInCurrentTab =
!await _items[_selectedIndex].navigationKey.currentState.maybePop();
if (isFirstRouteInCurrentTab) {
if (_selectedIndex != 0) {
_onItemTapped(1);
return false;
}
}
/// let system handle back button if we're on the first route
return isFirstRouteInCurrentTab;
},
child: Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _items
.map((e) => _navigationTab(
navigationKey: e.navigationKey, widget: e.widget))
.toList(),
),
bottomNavigationBar: BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: 'Screen 1,
),
BottomNavigationBarItem(
label: 'Screen 2,
),
BottomNavigationBarItem(
label: 'Screen 3,
),
BottomNavigationBarItem(
label: 'Screen 4,
),
],
currentIndex: _selectedIndex,
showUnselectedLabels: true,
onTap: _onItemTapped,
),
),
);
}
}
And you screens:
class Screen1 extends StatelessWidget {
final GlobalKey<ScaffoldState> scaffoldKey;
Screen1({required this.scaffoldKey});
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
backgroundColor: Colors.white,
drawer: Drawer(), // so this is what I want to close on BNB button press in each of the 4 screens
appBar: AppBar( // each screen has its own app bar
title: Text('Screens 1-4'),
),
body: Text('Body of Screens 1-4'),
);
}
}
I changed the list of screens _items to a late variables so you can pass the scaffoldKeys to them when declaring them.
I have a bottomNavigatorBar in my app, and on some screens I don't make it visible, so create a static method to handle it from any class. it works, but when I use the instance of this static method in onWillPop I have the following problems...
E/flutter (10927): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: setState() called in constructor: _telaPrincipalState#cb9c4(lifecycle state: created, no widget, not mounted)
E/flutter (10927): This happens when you call setState() on a State object for a widget that hasn't been inserted into the widget tree yet. It is not necessary to call setState() in the constructor, since the state is already assumed to be dirty when it is initially created
I already used mounted, but it still didn't work
if(!this.mounted){
}
Here is the 3 page code where I use routes
class AtivarEmailPage extends StatefulWidget {
static const String route = "/ativarEmail";
#override
_AtivarEmailPageState createState() => _AtivarEmailPageState();
}
class _AtivarEmailPageState extends State<AtivarEmailPage> {
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
if (!mounted) {
setState(() => telaPrincipal.show());
}
Navigator.of(context).pushNamedAndRemoveUntil(
HomeView.route, (Route<dynamic> route) => false,
arguments: null);
},
child: Scaffold(code...),);
}
}
the problem happens when I call onWillpop to return to HomeView
class HomeView extends StatefulWidget {
static const String route = "/";
#override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
Widget home(BuildContext context) {
return Scaffold(code..);
}
}
the bottomNavigatorBar manipulated by the static method is in the code below. It is the screen that is under the whole stack, so the navigation bar is floating on top of the other pages
class telaPrincipal extends StatefulWidget {
telaPrincipal({this.categoria, this.exercicio});
static _telaPrincipalState tela = _telaPrincipalState();
#override
_telaPrincipalState createState() {
return tela;
}
static indexBar(int index) {
tela.onItemTapped(index);
}
static void hide() {
tela.hideNavBar();
}
static void show() {
print("show");
tela.showNavBar();
}
}
class _telaPrincipalState extends State<telaPrincipal> {
void hideNavBar() {
setState(() {
_show = false;
_bottomBarHeight = 0;
});
}
void showNavBar() {
setState(() {
_show = true;
_bottomBarHeight = 60;
});
}
void onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
int _selectedIndex = 0;
final navigatorKey = GlobalKey<NavigatorState>();
//bottomNavigatorBar routes
final pagesRouteFactories = {
HomeView.route: () => MaterialPageRoute(builder: (context) => HomeView()),
programaTreino.route: () => MaterialPageRoute(
builder: (context) => programaTreino(
exercicioEscolhido: null,
categoria: null,
)),
pesquisar_view.route: () =>
MaterialPageRoute(builder: (context) => pesquisar_view()),
};
bool _show = true;
double _bottomBarHeight = 60;
#override
Widget build(BuildContext context) {
return WillPopScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: HexColor("#121212"),
body: _buildBody(),
bottomNavigationBar: Container(
height: _bottomBarHeight,
width: MediaQuery.of(context).size.width,
child: _show
? _buildBottomNavigationBar(context)
: Container(
color: Colors.white,
width: MediaQuery.of(context).size.width,
),
)),
),
onWillPop: () async {
navigatorKey.currentState.maybePop();
return false;
},
);
}
Widget _buildBody() => MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: navigatorKey,
onGenerateRoute: (settings) {
String routeName = settings.name;
//Map<String, dynamic> args = route.arguments; // Get any arguments passed to the route
print("ongenate$routeName}");
switch (routeName) {
case AtivarEmailPage.route:
return MaterialPageRoute(builder: (context) => AtivarEmailPage());
break;
default:
return pagesRouteFactories[settings.name]();
break;
}
});
Widget _buildBottomNavigationBar(context) => BottomNavigationBar(
currentIndex: _selectedIndex,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home_filled),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.fitness_center),
label: 'Treinos',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Pesquisar',
),
],
onTap: (routeName) {
print(pagesRouteFactories.keys.toList()[routeName]);
navigatorKey.currentState.pushNamed(
pagesRouteFactories.keys.toList()[routeName],
arguments: routeName == 0 ? null : [null, null]);
onItemTapped(routeName);
},
backgroundColor: HexColor("#FFFFFF").withOpacity(0.08),
selectedItemColor: HexColor("#FFCC80"),
unselectedItemColor: HexColor("#FFFFFF").withOpacity(0.30),
);
}
screens:
HomeView
AtivarEmail
I would like to go back with the bottomNavigatorBar visible to Homeview after pressing the android back button on the ActivateEmail screen
so create a static method to handle it from any class.
The short answer is: don't do that.
Read up on flutter state management. Pick the one you like best, but don't home-brew a solution.
It looks like you are using mounted in reverse. mounted is true when the widget is part of the tree. See The API documentation
Try
if (mounted) {
setState(() => telaPrincipal.show());
}
instead of if (!mounted)
I'm trying to build a custom bottom navigation system using the CupertinoTabBar and its working decently well, but I've come across a problem with the backbitten on Android.
When I have windows open, back button properly closes my screens (navigates to the previous screen) but when I'm at the very last screen, if you press back the whole app will close.
I'd like to try to prevent this
Is there a good way for me to detect when I am at my base window and prevent this in the WillPopScope area?
Heres my bottom navigation bar code as it stands
class MarkBottomNav2 extends StatefulWidget {
#override
State<MarkBottomNav2> createState() => _MarkBottomNavState2();
}
class _MarkBottomNavState2 extends State<MarkBottomNav2> {
final GlobalKey<NavigatorState> firstTabNavKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> secondTabNavKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> thirdTabNavKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> fourthTabNavKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> fifthTabNavKey = GlobalKey<NavigatorState>();
late CupertinoTabController tabController;
int index = 0;
#override
void initState() {
// TODO: implement initState
super.initState();
tabController = CupertinoTabController(initialIndex: 0);
}
#override
Widget build(BuildContext context) {
//making a list of the keys
final listOfKeys = [
firstTabNavKey,
secondTabNavKey,
thirdTabNavKey,
fourthTabNavKey,
fifthTabNavKey,
];
List homeScreenList = [
NewsArea(),
Area2(),
Area3(),
Area4(),
Area5()
];
return WillPopScope(
onWillPop: () async {
return !await listOfKeys[tabController.index].currentState!.maybePop();
},
child: CupertinoTabScaffold(
controller: tabController, //set tabController here
tabBar: CupertinoTabBar(
items: [
///this is where we are setting aur bottom ICONS
BottomNavigationBarItem(
icon: Icon(FontAwesome5.newspaper),
label: 'News',
),
BottomNavigationBarItem(icon: Icon(Icons.format_list_bulleted), label: 'Area 2'),
BottomNavigationBarItem(icon: Icon(Icons.mail_outline_outlined), label: 'Area 3'),
BottomNavigationBarItem(icon: Icon(Icons.alternate_email), label: 'Area 4'),
BottomNavigationBarItem(icon: Icon(Icons.more_horiz), label: 'Area 5'),
],
activeColor: Color.fromRGBO(26, 70, 128, 1),
inactiveColor: Color.fromRGBO(26, 70, 128, 0.3),
iconSize:20,
// currentIndex: pageIndex,
),
tabBuilder: (
context,
index,
) {
return CupertinoTabView(
navigatorKey: listOfKeys[
index], //set navigatorKey here which was initialized before
builder: (context) {
return homeScreenList[index];
},
);
},
),
);
}
}
You can try with the below code and use this first page where from page exit
DateTime currentBackPressTime;
#override
Widget build(BuildContext context) {
return Scaffold(
...
body: WillPopScope(child: getBody(), onWillPop: onWillPop),
);
}
Future<bool> onWillPop() {
DateTime now = DateTime.now();
if (currentBackPressTime == null ||
now.difference(currentBackPressTime) > Duration(seconds: 2)) {
currentBackPressTime = now;
Fluttertoast.showToast(msg: exit_warning);
return Future.value(false);
}
return Future.value(true);
}
I have a bottom navigation bar in my flutter app which is used to show different pages. I need to click on a button in one of the pages to navigate to another which can also be navigated through the bottom navigation bar. To keep the state of the page i have used IndexedStack widget. also i highlight which page i am currently at.
How to do so.
Here is the code.
class IndexPage extends StatefulWidget {
#override
_IndexPageState createState() => _IndexPageState();
}
class _IndexPageState extends State<IndexPage>
with SingleTickerProviderStateMixin {
final ValueNotifier<int> pageNumberNotifier = ValueNotifier<int>(0);
final List<Widget> _widgets = <Widget>[
Page1(),
Page2(),
Page3(),
];
#override
void dispose() {
super.dispose();
pageNumberNotifier.dispose();
}
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: pageNumberNotifier,
builder: (BuildContext context, int pageNumber, Widget child) {
return SafeArea(
child: Scaffold(
body: IndexedStack(
index: pageNumberNotifier.value,
children: _widgets,
),
bottomNavigationBar: BottomNavigationBar(
showUnselectedLabels: true,
currentIndex: pageNumber,
onTap: (index) => pageNumberNotifier.value = index,
items: <BottomNavigationBarItem>[
bottomNavigationBarItem(
iconString: 'page1', index: 0, title: 'Page1'),
bottomNavigationBarItem(
iconString: 'page2', index: 1, title: 'Page2'),
bottomNavigationBarItem(
iconString: 'page3', index: 2, title: 'Page3'),
],
),
),
);
});
}
BottomNavigationBarItem bottomNavigationBarItem(
{String iconString, int index, String title}) {
//shows the icons and also highlights the icon of the current page based on the current index.
}
}
Here is the page that contains the button
class Page1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
...
onTap(){
// go to Page3 and also highlight Page3 icon in the bottom navigation bar
}
...
}
}
Use a GlobalKey
GlobalKey<_IndexPageState> key = GlobalKey();
make a function inside _IndexPageState:
void setPage(int page) {
pageNumberNotifier.value = page;
}
call this method from anywhere with:
key.currentState.setPage(0);
I want to load pages from a List and when the user taps on an item from the drawer he can go to that page (if it's already opened) otherwise the Widget will load in the selected page.
But I can't find if that widget is already exists in the List if(myList.contains(Widget1())) => print('it exist'); One guy told me to override hashCode and operator==
class Widget6 extends StatelessWidget {
final String title = 'Widget6';
final Icon icon = Icon(Icons.assessment);
#override
Widget build(BuildContext context) {
return Center(
child: icon,
);
}
#override
bool operator ==(dynamic other) {
final Widget6 typedOther = other;
return title == typedOther.title && icon == typedOther.icon;
}
#override
int get hashCode => hashValues(title, icon);
}
if I do that I can't use any child widget to those widgets. Getting exception like: type 'Center' is not a subtype of type 'Widget6'. I copied this from flutter gallery I didn't find good documentation/guide. Sorry, I am a beginner.
Complete code below
class _MyHomePageState extends State<MyHomePage> {
List pageList = [
Widget1(),
Widget2(),
Widget3(),
Widget4(),
];
PageController _pageController;
int _selectedIndex = 0;
#override
void initState() {
_pageController = PageController(
initialPage: _selectedIndex,
);
super.initState();
}
void navigatePage(Widget widget) {
// problem is here
if (pageList.contains(widget)) {
_pageController.animateToPage(pageList.indexOf(widget, 0),
duration: Duration(milliseconds: 300), curve: Curves.ease);
}
else {
setState(() {
pageList.removeAt(_pageController.page.toInt());
pageList.insert(_pageController.page.toInt(), widget);
});
_pageController.animateToPage(_pageController.page.toInt(),
duration: Duration(milliseconds: 300), curve: Curves.ease);
}
Navigator.pop(context);
}
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: ListView(
children: <Widget>[
ListTile(
title: Text('Widget1'),
onTap: () => navigatePage(
Widget1(),
),
),
ListTile(
title: Text('Widget2'),
onTap: () => navigatePage(
Widget2(),
),
),
ListTile(
title: Text('Widget3'),
onTap: () => navigatePage(
Widget3(),
),
),
ListTile(
title: Text('Widget4'),
onTap: () => navigatePage(
Widget4(),
),
),
ListTile(
title: Text('Widget5'),
onTap: () => navigatePage(
Widget5(),
),
),
ListTile(
title: Text('Widget6'),
onTap: () => navigatePage(
Widget6(),
),
),
],
),
),
appBar: AppBar(
title: Text(widget.title),
),
body: PageView.builder(
onPageChanged: (newPage) {
setState(() {
this._selectedIndex = newPage;
});
},
controller: _pageController,
itemBuilder: (context, index) {
return Container(
child: pageList[index],
);
},
itemCount: pageList.length,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) => setState(() {
_selectedIndex = index;
_pageController.animateToPage(index,
duration: Duration(milliseconds: 300), curve: Curves.ease);
}),
items: pageList.map((page) {
return BottomNavigationBarItem(
backgroundColor: Colors.deepOrangeAccent,
icon: page.icon,
title: Text(page.title));
}).toList(),
),
);
}
}
Here List of dummy Widgets
class Widget1 extends StatelessWidget {
final String title = 'Widget1';
final Icon icon = Icon(Icons.school);
#override
Widget build(BuildContext context) {
return Center(
child: icon,
);
}
}
class Widget2 extends StatelessWidget {
// only title and icon are changed
}
class Widget3 extends StatelessWidget {
// only title and icon are changed
}
class Widget4 extends StatelessWidget {
// only title and icon are changed
}
class Widget5 extends StatelessWidget {
// only title and icon are changed
}
class Widget6 extends StatelessWidget {
// only title and icon are changed
}
Okay, I found the solution. And it has to do with operator== overriding
I missed this line if (runtimeType != other.runtimeType) return false;
The whole code stays the same.
#override
// ignore: hash_and_equals
bool operator ==(dynamic other) {
if (runtimeType != other.runtimeType) return false;
final Widget6 typedOther = other;
return title == typedOther.title;
}
#Ahmed Sorry for the late reply, I decided to put it in an answer rather than a comment.
One solution is yours, overriding == but I was thinking of using Key and then instead of using contains method, using something like:
if(myList.indexWhere((Widget widget)=> widget.key==_key) != -1)...
Suggestion
You can store icon and title as a map or a module instead of making 6 different Widget.
You can create another file, saying module.dart like this:
class Module {
final String title;
final Icon icon;
Module(this.title, this.icon);
#override
int get hashCode => hashValues(title.hashCode, icon.hashCode);
#override
bool operator ==(other) {
if (!identical(this, other)) {
return false;
}
return other is Module &&
this.title.compareTo(other.title) == 0 &&
this.icon == other.icon;
}
}
Then create another file that builds the page, saying mywidget.dart, like this:
class MyWidget extends StatelessWidget {
final Module module;
MyWidget({Key key,#required this.module}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: module.icon,
);
}
}
Then on each ListTile's onTap, Navigate like this:
...
ListTile(
title: Text('Widget1'),
onTap: () => navigatePage(
MyWidget(module: Module('Widget1', Icon(Icons.school)),)
),
),
...
So instead of storing Widgets, you store a Type(Here Module) that you declared.
You can also use the list's map to build each ListTile of the ListView for each Module, instead of doing it one by one. (if each item on the drawer are similar), Something like this:
List<Module> myTabs = [
Module('Widget1', Icon(Icons.school)),
Module('Widget2', Icon(Icons.home)),
];
...
Drawer(
child: ListView(
children:myTabs.map((Module module)=> ListTile(
title:Text( module.title),
onTap: navigatePage(MyWidget(module: module,)),
)).toList(),
) ,
);
...