I'm fairly new to flutter and am having trouble getting my BottomNavigationBar to update when my app receives a notification (eg. a new message in the users inbox). I've tried a couple solutions, like adding a listener in the 'initState' function, but it seems to cause a memory leak issue.
Here is my basic structure of the app in pseudo code:
main.dart
import 'globals.dart' as globals;
#override
void initState() {
addListeners();
super.initState();
}
void addListeners() {
FirebaseMessaging.onMessage.listen((RemoteMessage event) {
switch (event.data["type"]) {
case 'message':
debugPrint("New messages received");
globals.myInbox.value = data["messageCount"];
break;
}
}
}
... [All routes defined here] ...
layout.dart
import 'globals.dart' as globals;
class StandardLayout extends StatefulWidget {
Widget InboxCount() {
int messageCount = globals.myInbox;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Colors.blue,
),
body: Container(
height: double.infinity,
width: double.infinity,
color: Colors.white60,
child: widget.output,
),
bottomNavigationBar:
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
... [Other config options] ..
items: [
BottomNavigationBarItem(
label: 'Home',
icon: Icon(Icons.home),
),
BottomNavigationBarItem(
label: 'Schedule',
icon: Icon(Icons.calendar_today),
),
BottomNavigationBarItem(
label: 'Time Off',
icon: Icon(Icons.beach_access),
),
BottomNavigationBarItem(
label: 'Messages',
icon: Stack(
children: [
Icon(Icons.messenger),
InboxCount() //Stacks the badge on this item
],
)
)
],
)
);
}
}
example.dart
import 'layout.dart';
class ExampleScreen extends StatelessWidget {
const ExmapleScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StandardLayout('title': 'Example Screen', 'output': Container(child: Text('Hello World!')));
}
}
Attempt #1:
If I add a listener to the layout.dart like so:
#override
void initState() {
globals.myInbox.addListener(() {
//Refresh the layout
debugPrint('Reloading Layout');
setState(() {});
});
super.initState();
}
it sort of works, but it fires mutliple times as I can see the debugPrint message being displayed 2,3,4,5 even upto 6 times. I will also sometimes get messages saying the state is unreachable, been disposed, or warnings about memory leaks. So obviously it is getting fired too many times.
Attempt #2
If I add the listener to the main.dart file, the listener event only gets fired once when the globals.myInbox value changes, so thats good! Unfortunately, the bottom navigation in layout.dart doesn't get updated obviously because I can't send a 'setState' to another file.
So now what?
Is my structure of a dart/flutter program completely wrong that it won't allow me to do what I want? Where do I put the listener or how do I implement it so the bottom navigation will update when "globals.myInbox" gets updated?
Hopefully that makes sense, I omitted a bunch of code that isn't relevant to the problem.
Related
I have flutter widget created with below code, where Tabs are being created as shown below code snippet without having keys in it.
How to find Settings tab & click on it? Can this be done using ancestor or descendant?
Here is the image of the tabs
Here is the app code snippet.
// Imports...
class BottomNavigation extends StatelessWidget {
final TabItem currentTab;
final ValueChanged<TabItem> selectedTab;
const BottomNavigation({
super.key,
required this.currentTab,
required this.selectedTab,
});
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: TabItem.values.indexOf(currentTab),
type: BottomNavigationBarType.fixed,
items: [
_buildItem(TabItem.home, context),
_buildItem(TabItem.creditHealth, context),
_buildItem(TabItem.setting, context),
],
onTap: (index) => selectedTab(
TabItem.values[index],
),
);
}
BottomNavigationBarItem _buildItem(
TabItem item,
BuildContext context
// var toolTip,
) {
return BottomNavigationBarItem(
// tooltip: toolTip,
icon: SvgPicture.asset(
svgF(tabIcons[item]!),
),
label: context.translate(
tabNameKeys[item]!,
),
);
}
}
You should be able to tap by text:
import 'package:flutter_test/flutter_test.dart'
void main() {
testWidgets('signs up', (WidgetTester tester) async {
await tester.pumpWidget(YourApp());
await tester.tap(find.text('Settings'));
}
If you support languages other than English, then it's prudent to assign a key to the widgets you want to interact with, and then use these keys in tests.
// widget code
BottomNavigationBarItem(
key: Key('settings'),
icon: SvgPicture.asset(svgF(tabIcons[item]!)),
label: context.translate(tabNameKeys[item]!),
);
// test code
await tester.tap(find.byKey(Key('settings')));
I'm using syncfusion_flutter_pdfviewer package for my flutter project, and I'm trying to pass a specific page number from my Main Page to my Details page. After that it'll load that PDF page number immediacy when the Detail page is open, but I'm stuck, so any help or suggestion would be really appreciated.
I have tried like this, but I'm getting an error on "jumpToPage"
This expression has a type of 'void' so its value can't be used.
Try checking to see if you're using the correct API; there might be a function or call that returns void you didn't expect. Also check type parameters and variables which might also be void.
body: SfPdfViewer.asset(
'data/hymn_pdf/full-songs.pdf',
controller: _pdfViewerController.jumpToPage(widget.number),
),
Full Details page Code.
class DisplayScreen extends StatefulWidget {
final int number;
const DisplayScreen(this.number);
#override
_DisplayScreen createState() => _DisplayScreen();
}
class _DisplayScreen extends State<DisplayScreen> {
final GlobalKey<SfPdfViewerState> _pdfViewerKey = GlobalKey();
late PdfViewerController _pdfViewerController;
#override
void initState() {
_pdfViewerController = PdfViewerController();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Syncfusion Flutter PdfViewer'),
actions: <Widget>[
IconButton(
icon: Icon(
Icons.arrow_drop_down_circle,
color: Colors.white,
),
onPressed: () {
_pdfViewerController.jumpToPage(widget.number); // I got this from synfusion doc and I'm trying to copy it in my controller but I'm getting an error.
},
),
],
),
body: SfPdfViewer.asset(
'data/hymn_pdf/full-songs.pdf',
controller: _pdfViewerController.jumpToPage(widget.number), //I'm getting an error here
),
);
}
}
You cannot just call jumpToPage when assigning the controller. Wrong example:
body: SfPdfViewer.asset(
'data/hymn_pdf/full-songs.pdf',
controller: _pdfViewerController.jumpToPage(widget.number), // <-- nope
),
You need to call the method after the pdf is loaded. For example, call it in the onDocumentLoaded callback function.
Example code:
SfPdfViewer.asset(
'data/hymn_pdf/full-songs.pdf',
controller: _pdfViewerController,
onDocumentLoaded: (details) { // as soon as the doc is loaded
_pdfController.jumpToPage(3); // jump to page 3
},
)
My app allows people to post text and switch between different pages on a navbar. On the users page, there is a button, when clicked, will show an overlay so the user can create a post. The overlay includes a back button that calls a function to close the overlay. I want to keep the navbar available at the bottom so user can back out of the post that way if they want to.
The problem is, when the user uses the navbar, the overlay does not close because the close overlay function is on the user page and the navbar page does not have access to it.
How do I give another class on another dart file access to a method or function? If you are able to answer, can you please use my code instead of another example to help me follow better? Thank you.
User Page File #1
class UserPage extends StatefulWidget {
#override
_UserPageState createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
OverlayEntry? entry;
#override
Widget build(BuildContext context) {
return Scaffold(
ElevatedButton(
child: const Text('New Post'),
onPressed: showOverlay,
),
),
}
void showOverlay() {
(...)
}
void closeOverlay() {
entry?.remove();
entry = null;
}
}
Nav Bar File #2 (Need help with "OnTap")
class Nav extends StatefulWidget {
const Nav({Key? key}) : super(key: key);
#override
_NavState createState() => _NavState();
}
class _NavState extends State<Nav> {
int currentTab = 1; // makes the home page the default when loading up the app
#override
void initState() {
super.initState();
}
List<Widget> tabs = <Widget>[
const Other(),
const Home(),
const UserPage(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: tabs.elementAt(currentTab),
),
// BOTTOM NAVIGATION BAR
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentTab,
onTap: (value) {
setState(() => currentTab = value);
const _UserPageState().closeOverlay(); //HERE IS WHERE I NEED HELP WITH THE CODE
},
items: const [
BottomNavigationBarItem(
label: 'Other',
),
BottomNavigationBarItem(
label: 'Home',
),
BottomNavigationBarItem(
label: 'User Page',
),
],
),
);
}
}
You can try to make your _UserPageState public by removing - from it, and then call it UserPageState().closeOverlay();
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.
I want to send data from widget to another widget, in my example i want to send some filter data from FilterScreen.dart to ShopScreen.dart
it works fine but i dont know is what i'm doing is correct?
in filter model file:
class FilterData with ChangeNotifier {
bool isFreeShipping;
bool isSomeThingElse;
FilterData({this.isFreeShipping = false, this.isSomeThingElse = false});
void setFreeShippingValue(bool newval) {
isFreeShipping = newval;
notifyListeners();
}
void setSomeThingElseValue(bool newval) {
isSomeThingElse = newval;
notifyListeners();
}
}
in main.dart:
return ChangeNotifierProvider(
create: (context) => FilterData(),
child: MaterialApp(
.........
)
);
in tabs screen:
class TabsScreen extends StatefulWidget {
#override
_TabsScreenState createState() => _TabsScreenState();
}
class _TabsScreenState extends State<TabsScreen> {
List<Map<String, Object>> _pages;
int _selectedPageIndex = 0;
#override
void initState() {
_pages = [
{
'page': ShopScreen(),
'title': 'shop',
},
{
'page': FilterScreen(),
'title': 'filter',
},
];
super.initState();
}
void _selectPage(int index) {
setState(() {
_selectedPageIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_pages[_selectedPageIndex]['title']),
),
drawer: DrawerApp(),
body: _pages[_selectedPageIndex]['page'],
bottomNavigationBar: BottomNavigationBar(
onTap: _selectPage,
backgroundColor: Theme.of(context).primaryColor,
unselectedItemColor: Colors.white,
selectedItemColor: Theme.of(context).accentColor,
currentIndex: _selectedPageIndex,
// type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
backgroundColor: Theme.of(context).primaryColor,
icon: Icon(Icons.shop),
title: Text('Shop'),
),
BottomNavigationBarItem(
backgroundColor: Theme.of(context).primaryColor,
icon: Icon(Icons.search),
title: Text('Filter'),
),
],
),
);
}
}
in FilterScreen.dart:
class FilterScreen extends StatefulWidget {
#override
_FilterScreenState createState() => _FilterScreenState();
}
class _FilterScreenState extends State<FilterScreen> {
#override
Widget build(BuildContext context) {
final data = Provider.of<FilterData>(context);
return Container(
child: Center(
child: Expanded(
child: ListView(
children: <Widget>[
SwitchListTile(
title: Text('Free Shipping'),
value: data.isFreeShipping,
subtitle: Text('get free shipping products'),
onChanged: (newValue) {
data.setFreeShippingValue(newValue);
}),
SwitchListTile(
title: Text('Some thing else'),
value: data.isSomeThingElse,
subtitle: Text('get filtred products'),
onChanged: (newValue) {
data.setSomeThingElseValue(newValue);
}),
],
),
),
),
);
}
}
in ShopScreen.dart:
class ShopScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final data = Provider.of<FilterData>(context);
return Container(
child: Center(
child: Text(
data.isFreeShipping ? 'get favorite Products' : 'get all products'),
),
);
}
}
enter image description here
Your question indeed is a pain for most of the developers, which is like I don't know how it works!
So, if you are not able to understand. there are two reasons to that:
You just blindly followed the tutorial or documentation, cos of the time constraints
You did not understand how Flutter Provider State Management works. So, for that, do read upon these:
List of state managements in flutter
Flutter provider package, of course you have used that in your project. But read how he is using.
So, now let us jump to the code. How your code works?
There are multiple things which are responsible for this:
1. Provider Wrap: If you closely look into the main.dart code, you have done this
return ChangeNotifierProvider(
create: (context) => FilterData(), // here you define the ChangeNotifier class
child: MaterialApp(
.........
)
);
Now looking at the above code, you see, whenever you wrap the app with the ChangeNotifierProvider(), it always rebuilds whenever there is a state change in the class which you have provided inside that, in this case FilterData(). Any changes happens will reflect in the whole app, cos, ChangeNotifierProvider(), is keep rebuilding the state of the immediate child, in this case your, MaterialApp(), which is wrapped.
2. NotifyChanges from the ChangeNotifier class: If you look at your FilterData, it is the one which is responsible for the rebuilding of the app, which is wrapped by the ChangeNotifierProvider().
Let us see how:
void setFreeShippingValue(bool newval) {
isFreeShipping = newval;
notifyListeners();
}
void setSomeThingElseValue(bool newval) {
isSomeThingElse = newval;
notifyListeners();
}
If you closely take a look at the methods, which I mentioned in the above code from your FilterData class only, they have notifyListeners(). These are the ones, which is responsible, whenever your two methods called, it notifies the ChangeNotifierListener to rebuild the widget, and hence you see the updated data every time, you use any of the two methods
3. Using NotifyListeneres method from the FilterData in FilterScreen: So, again if we look closely at the thing which we have mentioned in the point 2, we see that, the method method should be called to make changes in the App which is the immediate child of ChangeNotifierProvider()
SwitchListTile(
title: Text('Free Shipping'),
value: data.isFreeShipping,
subtitle: Text('get free shipping products'),
onChanged: (newValue) {
data.setFreeShippingValue(newValue);
}),
SwitchListTile(
title: Text('Some thing else'),
value: data.isSomeThingElse,
subtitle: Text('get filtred products'),
onChanged: (newValue) {
data.setSomeThingElseValue(newValue);
}),
So, when you call any of the methods in your onChanged, it straight away notifies the Provider that, the value has been changed, and the app rebuilds, and when you switch to the other tab, you see updated result like magic.
MOST IMPORTANT: Your final data = Provider.of<FilterData>(context);, is an instance of the Provider class, which trigger the method to help notify the ChangeNotifierProvider() to make changes in the app
So the mapping is like that:
Listens to the change
FilterData {setFreeShippingValue, setSomeThingElseValue} <----------------------> ChangeNotifierProvider() REBUILDS MATERIALAPP()