nylo-core / nylo

Nylo is the fastest way to build your next Flutter mobile app. Streamline your projects with Nylo's opinionated approach to building Flutter apps. Develop your next idea ⚡️
https://nylo.dev
MIT License
597 stars 61 forks source link

updateState does not work with bottomNavigationBar Pages #135

Closed KhaledAlMana closed 6 months ago

KhaledAlMana commented 6 months ago

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 use updateState(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 in bottomNavigationBar

I have checked the docs and the tutorials, but no luck. Am I missing something?

Thanks again for the great efforts.

KhaledAlMana commented 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.

KhaledAlMana commented 6 months ago

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?

agordn52 commented 6 months ago

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?

KhaledAlMana commented 6 months ago

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)
                    ],
                  ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
KhaledAlMana commented 6 months ago

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)
                    ],
                  ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
KhaledAlMana commented 6 months ago

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?

agordn52 commented 6 months ago

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!