SetState() on BottomNavigatonBar's onTap(index) method does not rebuild widget tree - flutter

I'm trying to connect a webview_flutter WebView to a BottomNavigationBar.
I want the view to be reloaded with a new URL whenever a tab is tapped. In
the onTap callback I update the _currentUrl to a new url and call set state with the new tab index.
Why is the tab bar updating but the web view is not rebuilt?
What did I miss?
class WebViewApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return WebViewState();
}
}
class WebViewState extends State<WebViewApp> {
int _currentTabIndex = 0;
String _currentUrl = URL.home;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter WebView Demo',
home: Scaffold(
body: WebView(initialUrl: _currentUrl),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentTabIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
title: Text('Search'),
),
BottomNavigationBarItem(
icon: Icon(Icons.calendar_today),
title: Text('Profile'),
),
],
onTap: (index) {
switch (index) {
case 0:
_currentUrl = URL.home;
break;
case 1:
_currentUrl = URL.search;
break;
case 2:
_currentUrl = URL.calendar;
break;
}
setState(() {
_currentTabIndex = index;
});
},
),
),
);
}
}

You can use the WebView's WebViewController to change url of the webview
Add following code to your WebView widget:
body: WebView(
initialUrl: _currentUrl,
onWebViewCreated: (WebViewController webController) {
_webController = webController;
},
),
Now after you have assiged the WebViewController to your instance of WebViewController, you can change the url of the current webview with the loadUrl() method of the WebViewController
So you can change the code of you onTap handler to following:
onTap: (index) {
switch (index) {
case 0:
_webController.loadUrl('your_home_url');
break;
case 1:
_webController.loadUrl('your_search_url');
break;
case 2:
_webController.loadUrl('your_calander_url');
break;
}
},
The problem with WebViews and setState is that the whole Widget / Page will be rebuild including the instance of WebView, but you only want to change the url in the current WebView session...

Thanks for the comment Joel
Yeah its pretty strange to publish a crossplatform framework and basic components does only work on one platform but yeah hopefully they will fix it until October as sad on their github...
I checked my post and the WebView should load the new url. The thing I forgot was to update the _currentTabIndex in the onTap handler of the navigation.
But anyhow: I made a quick example of your case. It works fine for me...
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WebView App',
debugShowCheckedModeBanner: false,
home: WebPage(),
);
}
}
class WebPage extends StatefulWidget {
int currentTabIndex = 0;
#override
_WebPageState createState() => _WebPageState();
}
class _WebPageState extends State<WebPage> {
List<String> _urls = [
'https://www.zdf.de/nachrichten/heute/rubrik-politik-100.html',
'https://www.zdf.de/nachrichten/heute/rubrik-wirtschaft-100.html',
'https://www.zdf.de/nachrichten/heute/rubrik-panorama-100.html'
];
WebViewController _webController;
#override
Widget build(BuildContext context) {
print('<----- build init ----->');
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('WebView'),
),
body: WebView(
initialUrl: _urls[widget.currentTabIndex],
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webController) {
_webController = webController;
},
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: widget.currentTabIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.people),
title: Text('Politik')
),
BottomNavigationBarItem(
icon: Icon(Icons.public),
title: Text('Wirtschaft')
),
BottomNavigationBarItem(
icon: Icon(Icons.pie_chart_outlined),
title: Text('Panorama')
)
],
onTap: (selectedIndex) {
_webController.loadUrl(_urls[selectedIndex]);
setState(() {
widget.currentTabIndex = selectedIndex;
});
},
),
);
}
}
Note that I call the setState() at the end of the onTap handler which rebuilds the page, but strangly the WebView will not be re initialized!? I thought that that must be the case, but flutter must be handling this in someway or another...
Also note that I moved your _currentTabIndex variable from your WebViewState class to its upper parent class WebViewApp. The problem was, that if you rebuild the page via setState() the _currentTabIndex would always be re initialized with 0. For tutorial's sake I just moved it upwards, but maybe you could use it as global parameter...
But yeah the WebView is still a bit confusing and it got lots of differences between the iOS and Android WebView kits (at the end of the day, the flutter WebView is "only" a wrapper of the current native WebView kit)
I hope this helps and good luck!

Related

Flutter BottomNavigationBar call navigator push for one Navigator item

I have a BottomNavigationBar with 4 BottomNavigaton items and want 3 items to render different contents for the body, which works fine already. I want the first item to open the camera and link to a completely new page. How can I do this?
What I already tried is attached below.
I get errors like
setState() or markNeedsBuild() called during build. This Overlay widget cannot be marked as needing to build because the framework is already in the process of building widgets.
So I think I call the build method of CameraScreen too early, but I dont know how to avoid it.
class TabScreen extends StatefulWidget {
int index;
TabScreen(this.index);
#override
_TabScreenState createState() => _TabScreenState(index);
}
class _TabScreenState extends State<TabScreen> {
int _selectedPageIndex;
_TabScreenState([this._selectedPageIndex = 1]);
final List<Map<String, Object>> _pages = [
{
// index = 0 should push new Screen without appbar & bottom nav bar and open camera
'page': null,
'title': 'Cam',
},
{
'page': ListScreen(),
'title': 'List',
},
{
'page': TransportScreen(),
'title': 'Transport',
},
{
'page': ExportScreen(),
'title': 'Export',
}
];
void _selectPage(int index, BuildContext ctx) {
setState(() {
_selectedPageIndex = index;
});
// this part does not work
// if (_selectedPageIndex == 0){
// Navigator.of(ctx).pushNamed(CameraScreen.routeName);
// }
}
#override
Widget build(BuildContext context) {
final bottomBar = BottomNavigationBar(
currentIndex: _selectedPageIndex,
onTap: (i) => _selectPage(i, context),
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.camera_alt_outlined),
label: 'Cam',
// backgroundColor: Colors.red,
),
BottomNavigationBarItem(
icon: Icon(Icons.article_outlined),
label: 'List',
),
BottomNavigationBarItem(
icon: Icon(Icons.article_outlined),
label: 'Transport',
),
BottomNavigationBarItem(
icon: Icon(Icons.arrow_forward),
label: 'Export',
),
],
);
return Scaffold(
appBar: AppBar(
title: Text(_pages[_selectedPageIndex]['title']),
),
body: _pages[_selectedPageIndex]['page'],
bottomNavigationBar: BottomAppBar(
shape: CircularNotchedRectangle(),
notchMargin: 4,
clipBehavior: Clip.antiAlias,
child: bottomBar,
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: _buildActionButton(),
);
}
}
Well I solved it myself. The above solution which uses Navigator.of(ctx).pushNamed(CameraScreen.routeName);
works indeed.
My problem was in the CameraScreen File, which used some Widgets in its build function and became too large to fit on the screen.

flutter_pagewise library does not preserve page (scroll) state on tabbing away

I am using the flutter_pagewise library and have implemented a paginated grid as per the library's documentation (following their example at https://pub.dev/packages/flutter_pagewise/example), which grabs placeholder images and text via the network.
In my app, I have 2 pages (one is called PaginatedGrid and the other is called SearchPage) that I can tab to via a BottomNavigationBar. However, when I tab to the SearchPage, then tab back to PaginatedGrid, the paginated grid scroll state isn't preserved. The pagination starts from the very beginning and the screen is scrolled back to the top.
import 'package:myproject/my_events/my_events_page.dart';
import 'package:myproject/search/search_page.dart';
import 'package:myproject/widget/paginated_grid.dart';
import 'package:flutter/material.dart';
class PageWrapper extends StatefulWidget {
#override
_PageWrapperState createState() => _PageWrapperState();
}
class _PageWrapperState extends State<PageWrapper> {
ScrollController _scrollController = ScrollController();
int _curIndex = 0;
List<Widget> _pages;
final bucket = PageStorageBucket();
final Key searchPageKey = PageStorageKey('searchKey');
final Key paginatedGridKey = PageStorageKey('paginatedGrid');
#override
void initState() {
_pages = [
PaginatedGrid(key: paginatedGridKey),
SearchPage(key: searchPageKey)
];
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageStorage(
bucket: bucket,
child: CustomScrollView(
key: PageStorageKey(_pages[_curIndex].runtimeType.toString()),
controller: _scrollController,
slivers: <Widget>[SliverAppBar(), _pages[_curIndex]],
),
),
bottomNavigationBar: BottomNavigationBar(
onTap: (int i) {
setState(() {
_curIndex = i;
});
},
currentIndex: _curIndex,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Browse',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Search',
)
],
),
);
}
}
Any help would be appreciated.
Using an indexedStack is a solution that worked! The paginated state is preserved on navigation to another tab from the bottom navigation bar.
Instead of using a PageStorage widget, use an IndexedStack widget.
#override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: currentTab,
children: pages,
),
bottomNavigationBar: BottomNavigationBar(
The solution is described here: https://medium.com/#codinghive.dev/keep-state-of-widgets-with-bottom-navigation-bar-in-flutter-bb732214bd11

Flutter send and show data between BottomNavigationBarItem

I want to share and show data between BottomNavigationBarItems
It is my BottomNavigationBar page:
class BottomNavigationBarController extends StatefulWidget {
#override
_BottomNavigationBarControllerState createState() =>
_BottomNavigationBarControllerState();
}
class _BottomNavigationBarControllerState
extends State<BottomNavigationBarController> {
List<Widget> pages = [];
#override
void initState() {
pages.add(HomePage(
key: PageStorageKey('HomePage'),
));
pages.add(MapPage(
key: PageStorageKey('MapPage'),
));
super.initState();
}
int _currentIndex = 0;
Widget _bottomNavigationBar(int selectedIndex) => BottomNavigationBar(
onTap: (int index) => setState(() => _currentIndex = index),
currentIndex: selectedIndex,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
BottomNavigationBarItem(icon: Icon(Icons.map), label: "MapPage"),
],
);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("iArrived"),
actions: _currentIndex != 1
? <Widget>[
IconButton(
icon: Icon(Icons.keyboard_arrow_right),
onPressed: () {
setState(() {
_currentIndex++;
});
})
]
: null),
backgroundColor: Colors.white,
bottomNavigationBar: _bottomNavigationBar(_currentIndex),
body: IndexedStack(
index: _currentIndex,
children: pages,
),
);
}
}
And I want to send my location from the MapPage to the HomePage.
There is a function in the MapPage that is executed when the place change:
void _setLocation(LatLng latlng) {
setState(() {
lat = latlng.latitude;
lng = latlng.longitude;
});
}
I think I have to call some function like a callback inside _setLocation() but I don't know how to implement it to show the location on my HomePage and have it refresh every time I change it.
Thanks for the help.
If your answer is to use shared_preferences plugin, can you tell me how would you implement it? Because I tried it and it did not work for me.
Thanks again.
You can use provider plugin for state management:
Create a model that store a shared value:
class LocationModel extends ChangeNotifier {
Location location;
void updateLocation(Location location) {
this.location = location;
notifyListeners();
}
}
Wrap your root widget with Provider class:
#override
Widget build(BuildContext build) {
return ChangeNotifierProvider(
create: (_) => LocationModel(),
child: App(),
);
}
When you changing a shared value in a model (you need to get a model from Provider, it depends on concrete implementation). For example:
onTap() {
model.updateLocation(newLocation):
}

Change tab controller index, and wait for the TabView in Flutter

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.

Flutter Remove Bottom Navigation Bar on Navigatore.push

I'm working on a project with flutter, I have a bottom navigation bar , this bar allow me to move on different scenes . I would like to remove my bottom navigation bar when i navigate to a detail page, there is any method? I'm trying all the possible solution , but i can't solve the problem, maybe i've implemented wrong my navigation bar !
Some Code:
My HomePage where i implemented the bottom navigation bar and where i call all the others pages:
class homeScreen extends StatefulWidget {
#override
_homeScreenState createState() => _homeScreenState();
}
class _homeScreenState extends State<homeScreen> {
TextEditingController _linkController;
int _currentIndex = 0;
final _pageOptions = [
meetingScreen(),
dashboardScreen(),
notificationScreen(),
profileScreen(),
];
#override
void initState() {
// TODO: implement initState
super.initState();
_linkController = new TextEditingController();
}
#override
Widget build(BuildContext context) {
ScreenUtil.instance = ScreenUtil(width: 1125, height: 2436)..init(context);
return MaterialApp(
title: myUi.myTitle,
theme: myUi.myTheme ,
debugShowCheckedModeBanner: false,
home:Scaffold(
bottomNavigationBar: _buildBottomNavigationBar(),
body: _pageOptions[_currentIndex] ,
) ,
);
}
Widget _buildBottomNavigationBar() {
return BottomNavigationBar(
backgroundColor: Colors.white,
selectedItemColor: Theme.of(context).primaryColor,
//fixedColor: gvar.secondaryColor[gvar.colorIndex],
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.explore,size: ScreenUtil().setWidth(80),),
title: new Text("Esplora"),
),
BottomNavigationBarItem(
icon:Icon(Icons.dashboard,size: ScreenUtil().setWidth(80),),
title: new Text("Attività"),
),
BottomNavigationBarItem(
icon:Icon(Icons.notifications,size: ScreenUtil().setWidth(80),),
title: new Text("Notifiche"),
),
BottomNavigationBarItem(
icon:Icon(Icons.person,size: ScreenUtil().setWidth(80),),
title: new Text("Profilo"),
),
],
);
}
}
This is the code i use into the dashboardScreen to push to my detail page:
onTap: () {
//method: navigate to Meeting
Navigator.push(context, MaterialPageRoute(
builder: (BuildContext context) => new meetingScreen()),
);
},
In the new meetingScreen i have some ui , but i cant remove my old bottom navigation bar, i tried implementing a new navigation bar but it isn't displayed.
A partial working solution was to do like so:
Navigator.of(context, rootNavigator: true).pushReplacement(MaterialPageRoute(builder: (context) => new meetingScreen()));
But now I can't do Navigator.pop with the cool animation , because the new page is the root!
Thanks guys for the support !!
To navigate to a different screen without the bottom Navigation bar, you'd need to call Navigator.push() from _homeScreenState. For you to be able to do this, you need to create a NavigatorKey in _homeScreenState and set it in MaterialApp().
final _mainNavigatorKey = GlobalKey<NavigatorState>();
...
MaterialApp(
navigatorKey: _mainNavigatorKey,
...
)
You can then pass the NavigatorKey to your pages. To use _mainNavigatorKey, fetch its current state to be able to invoke push().
navigatorKey.currentState!.push(...)
Here's a sample that I have. Though instead of a bottom Navigation bar, I'm using a Navigation Drawer. The same method applies in this sample.