Closed KhaledAlMana closed 6 months ago
Never mind, I believe my whole implementation of the navigation dashboard was corrupting the state.
I treated the Dashboard Page as a Layout that holds the actual pages, that's why the stateUpdated isn't invoked.
I made my UserNavigationBar widget to be used whenever is needed instead.
my approach of having UserNavigationBar creates a sort of flicker when navigating between navigation items.
If I use one page to navigate between Items it would be the same problem of creating this issue. and UserNavigationBar is causing a tiny flicker in navigation.
How to create a bottom navigation bar without causing a drawback?
Hi @KhaledAlMana,
Thanks for reporting this, I'm not sure what could be causing the flicker. Could you share your code in a repo? Or drop a snippet here?
Hello @agordn52,
Thanks for the reply.
Please find below.
Previous Approach (No flicker, but no state update):
// dashboard_page.dart
import 'package:flutter/material.dart';
import 'package:heroicons/heroicons.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:tahadu/resources/pages/new_item_page.dart';
import 'package:tahadu/resources/pages/user/claims_page.dart';
import 'package:tahadu/resources/pages/user/favorites_page.dart';
import 'package:tahadu/resources/pages/user/my_account_page.dart';
import 'package:tahadu/resources/pages/user/wishlists_page.dart';
class DashboardPage extends NyStatefulWidget {
static const path = '/dashboard';
DashboardPage() : super(path, child: _DashboardPageState());
}
class _DashboardPageState extends NyState<DashboardPage> {
int _currentIndex = 0;
List<String> _titles = [
WishlistsPage.title,
FavoritesPage.title,
'navigation_new_item',
ClaimsPage.title,
MyAccountPage.title,
];
List<String> _navTitles = [
WishlistsPage.navTitle,
FavoritesPage.navTitle,
'navigation_new_item',
ClaimsPage.navTitle,
MyAccountPage.navTitle,
];
final List<Widget> _pages = [
WishlistsPage(),
FavoritesPage(),
NewItemPage(),
ClaimsPage(),
MyAccountPage(),
];
/// Use boot if you need to load data before the [view] is rendered.
// @override
// boot() async {
// await Future.delayed(Duration(seconds: 2));
// }
@override
Widget view(BuildContext context) {
return Scaffold(
primary: false,
appBar: AppBar(
title: Text(_titles[_currentIndex].tr()),
),
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
selectedLabelStyle: TextStyle(fontWeight: FontWeight.bold),
showUnselectedLabels: true,
showSelectedLabels: true,
type: BottomNavigationBarType.fixed,
// backgroundColor: Colors.white,
elevation: 2,
selectedFontSize: 12,
onTap: (index) {
setState(() {
if (index == 2) {
_showBottomSheet();
return;
}
_currentIndex = index;
});
},
items: [
// wishlists
BottomNavigationBarItem(
icon: HeroIcon(
HeroIcons.sparkles,
style: (_currentIndex == 0)
? HeroIconStyle.solid
: HeroIconStyle.outline,
size: 28,
),
label: _navTitles[0].tr(),
),
// favorites
BottomNavigationBarItem(
icon: HeroIcon(
HeroIcons.star,
style: (_currentIndex == 1)
? HeroIconStyle.solid
: HeroIconStyle.outline,
size: 28,
),
label: _navTitles[1].tr(),
),
// add item
BottomNavigationBarItem(
icon: HeroIcon(
HeroIcons.plusCircle,
style: (_currentIndex == 2)
? HeroIconStyle.solid
: HeroIconStyle.outline,
size: 28,
),
label: _navTitles[2].tr(),
),
// claims
BottomNavigationBarItem(
icon: HeroIcon(
HeroIcons.checkBadge,
style: (_currentIndex == 3)
? HeroIconStyle.solid
: HeroIconStyle.outline,
size: 28,
),
label: _navTitles[3].tr(),
),
// my account
BottomNavigationBarItem(
icon: HeroIcon(
HeroIcons.userCircle,
style: (_currentIndex == 4)
? HeroIconStyle.solid
: HeroIconStyle.outline,
size: 28,
),
label: _navTitles[4].tr(),
),
],
),
);
}
void _showBottomSheet() {
showModalBottomSheet(
context: context,
builder: (context) => NewItemPage(),
// enableDrag: true,
isDismissible: true,
elevation: 2,
showDragHandle: true,
useSafeArea: true,
isScrollControlled: true,
);
}
}
One of the pages
// favorites_page.dart
import 'package:flutter/material.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:tahadu/app/controllers/favorites_controller.dart';
import 'package:tahadu/app/models/wishlist.dart';
import 'package:tahadu/resources/widgets/favorite_wishlist_card_widget.dart';
import 'package:tahadu/resources/widgets/safearea_widget.dart';
class FavoritesPage extends NyStatefulWidget {
static const path = '/favorites';
static const navTitle = 'navigation_favorites';
static const title = 'favorites_title';
FavoritesPage() : super(path, child: _FavoritesPageState());
@override
createState() => _FavoritesPageState();
}
class _FavoritesPageState extends NyState<FavoritesPage> {
/// [FavoritesController] controller
FavoritesController controller = FavoritesController();
List<Wishlist> wishlists = [];
@override
init() async {
// stateName = FavoritesPage.path;
controller.construct(context);
print("state update status:" + allowStateUpdates.toString());
}
/// Use boot if you need to load data before the view is rendered.
@override
boot() async {
print("state name " + super.stateName.toString());
wishlists = await controller.loadFavorites();
}
@override
stateUpdated(dynamic data) async {
print("State updated");
print(data);
if (data != null) {
if (data is List<Wishlist>) {
setState(() async {
wishlists = await data;
});
}
}
}
@override
Widget view(BuildContext context) {
return Scaffold(
body: SafeAreaWidget(
child: Container(
child: SingleChildScrollView(
child: Column(
children: [
if (controller.wishlists.length <= 0)
Center(
child: Text("You have no favorites yet", // TODO: Translate
style: TextStyle(fontSize: 16)),
),
// Wishlist Cards
if (controller.wishlists.length > 0)
Column(
children: [
for (var wishlist in controller.wishlists)
FavoriteWishlistCard(
wishlist: wishlist,
onTap: controller.removeFromFavorites)
],
),
],
),
),
),
),
);
}
}
The other approach (flickers, but state works): No more dashboard.
// user_scaffold_widget.dart
import 'package:flutter/material.dart';
import 'package:heroicons/heroicons.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:tahadu/resources/pages/user/claims_page.dart';
import 'package:tahadu/resources/pages/user/favorites_page.dart';
import 'package:tahadu/resources/pages/user/my_account_page.dart';
import 'package:tahadu/resources/pages/user/new_item_page.dart';
import 'package:tahadu/resources/pages/user/wishlists_page.dart';
class UserScaffoldWidget extends StatefulWidget {
UserScaffoldWidget({
Key? key,
this.appBar,
this.body,
this.currentIndex = 0,
}) : super(key: key);
final AppBar? appBar;
final Widget? body;
final int currentIndex;
@override
_UserScaffoldWidgetState createState() => _UserScaffoldWidgetState();
}
class _UserScaffoldWidgetState extends State<UserScaffoldWidget> {
int _selectedIndex = 0;
final List<String> _titles = [
WishlistsPage.title,
FavoritesPage.title,
'navigation_new_item',
ClaimsPage.title,
MyAccountPage.title,
];
final List<String> _pages = [
WishlistsPage.path,
FavoritesPage.path,
NewItemPage.path,
ClaimsPage.path,
MyAccountPage.path,
];
final List<String> _navTitles = [
WishlistsPage.navTitle,
FavoritesPage.navTitle,
'navigation_new_item',
ClaimsPage.navTitle,
MyAccountPage.navTitle,
];
@override
void initState() {
super.initState();
_selectedIndex = widget.currentIndex;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: widget.appBar ??
AppBar(
title: Text(_titles[_selectedIndex].tr()),
),
body: widget.body,
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
selectedLabelStyle: TextStyle(fontWeight: FontWeight.bold),
showUnselectedLabels: true,
showSelectedLabels: true,
type: BottomNavigationBarType.fixed,
elevation: 2,
selectedFontSize: 12,
onTap: _onTap,
items: [
// wishlists
BottomNavigationBarItem(
icon: HeroIcon(
HeroIcons.sparkles,
style: (_selectedIndex == 0)
? HeroIconStyle.solid
: HeroIconStyle.outline,
size: 28,
),
label: _navTitles[0].tr(),
),
// favorites
BottomNavigationBarItem(
icon: HeroIcon(
HeroIcons.star,
style: (_selectedIndex == 1)
? HeroIconStyle.solid
: HeroIconStyle.outline,
size: 28,
),
label: _navTitles[1].tr(),
),
// add item
BottomNavigationBarItem(
icon: HeroIcon(
HeroIcons.plusCircle,
style: (_selectedIndex == 2)
? HeroIconStyle.solid
: HeroIconStyle.outline,
size: 28,
),
label: _navTitles[2].tr(),
),
// claims
BottomNavigationBarItem(
icon: HeroIcon(
HeroIcons.checkBadge,
style: (_selectedIndex == 3)
? HeroIconStyle.solid
: HeroIconStyle.outline,
size: 28,
),
label: _navTitles[3].tr(),
),
// my account
BottomNavigationBarItem(
icon: HeroIcon(
HeroIcons.userCircle,
style: (_selectedIndex == 4)
? HeroIconStyle.solid
: HeroIconStyle.outline,
size: 28,
),
label: _navTitles[4].tr(),
),
],
));
}
void _onTap(int index) {
if (index == 2) {
_showBottomSheet();
return;
}
if (_selectedIndex == index) {
return;
}
setState(() {
_selectedIndex = index;
});
print("Selected Index: $_selectedIndex");
print("Current Index: $index");
routeTo(_pages[_selectedIndex],
navigationType: NavigationType.pushAndForgetAll,
pageTransition: PageTransitionType.fade,
pageTransitionSettings: PageTransitionSettings(
alignment: Alignment.bottomCenter,
));
}
void _showBottomSheet() {
showModalBottomSheet(
context: NyNavigator.instance.router.navigatorKey!.currentContext!,
builder: (context) => NewItemPage(),
// enableDrag: true,
isDismissible: true,
elevation: 2,
showDragHandle: true,
useSafeArea: true,
isScrollControlled: true,
);
}
}
Any page in the bottom navigation bar will use that widget.
// favorites_page.dart
import 'package:flutter/material.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:tahadu/app/controllers/favorites_controller.dart';
import 'package:tahadu/app/models/wishlist.dart';
import 'package:tahadu/resources/widgets/favorite_wishlist_card_widget.dart';
import 'package:tahadu/resources/widgets/safearea_widget.dart';
import 'package:tahadu/resources/widgets/user_scaffold_widget.dart';
class FavoritesPage extends NyStatefulWidget {
static const path = '/favorites';
static const navTitle = 'navigation_favorites';
static const title = 'favorites_title';
FavoritesPage() : super(path, child: _FavoritesPageState());
@override
createState() => _FavoritesPageState();
}
class _FavoritesPageState extends NyState<FavoritesPage> {
/// [FavoritesController] controller
FavoritesController controller = FavoritesController();
@override
init() async {
super.init();
controller.construct(context);
print("state update status:" + allowStateUpdates.toString());
}
/// Use boot if you need to load data before the view is rendered.
@override
boot() async {
print("state name " + super.stateName.toString());
await controller.loadFavorites();
}
@override
stateUpdated(dynamic data) async {
print("State updated");
print(data);
if (data != null) {
if (data is List<Wishlist>) {
setState(() async {
controller.wishlists = await data;
});
}
}
}
@override
Widget view(BuildContext context) {
return UserScaffoldWidget(
currentIndex: 1,
body: SafeAreaWidget(
child: Container(
child: SingleChildScrollView(
child: Column(
children: [
if (controller.wishlists.length <= 0)
Center(
child: Text("You have no favorites yet",
style: TextStyle(fontSize: 16)),
),
// Wishlist Cards
if (controller.wishlists.length > 0)
Column(
children: [
for (var wishlist in controller.wishlists)
FavoriteWishlistCard(
wishlist: wishlist,
onTap: controller.removeFromFavorites)
],
),
],
),
),
),
),
);
}
}
I came back to the first approach, and I converted the page to a regular widget and it worked. (Inspired by your state management video)
// part of: favorites_page.dart
class FavoritesPage extends StatefulWidget { // use StatefulWidget instead of NyStatefulWidget
static const path = '/favorites';
static const navTitle = 'navigation_favorites';
static const title = 'favorites_title';
FavoritesPage({Key? key}) : super(key: key); // change constructor accordingly
@override
createState() => _FavoritesPageState();
}
Does it make sense?
Hi @KhaledAlMana,
Thanks for sharing this.
There are a few code changes that I'd recommend but I'm glad you managed to resolve it!
Hello,
I have a dashboard page which holds the bottomNavigationBar. if I use
updateState(Dashboard.path, data)
stateUpdated
in dashboard is invoked but if I useupdateState(Page2.path, data)
stateUpdated
isn't invoked in Page2.After debugging: The current state is /dashboard at the beginning. But, if I go to Page2 and print the stateName, it's null. So, I set the value manually and it's no longer null. Yet,
stateUpdated
still isn't invoked.Any page that does not use bottom navigation bar,
stateUpdated
works just fine, except for the pages that are used inbottomNavigationBar
I have checked the docs and the tutorials, but no luck. Am I missing something?
Thanks again for the great efforts.