Update Scaffold-Properties in nested CupertinoPageScaffold in Flutter

Viewed 254

How can I update some properties of the roots scaffold in a child widget(page).

Here is a snippet from my root scaffold.

CupertinoPageScaffold(
      resizeToAvoidBottomInset:
          state.resizeToAvoidBottomInsets, //update this here
      child: CupertinoTabScaffold(
          controller: _tabController,
          tabBar: CupertinoTabBar(
            onTap: onTap,
            items: widget.items,
          ),
          tabBuilder: (BuildContext context, index) {
            return StatusBarPadding(child: _tabs[index]);
          }),
    ),

The docs say, I should add a listener to avoid a nested scaffold (e.g. to update resizeToAvoidBottomInset). However, this does only work for one page per tab. When I nest tabs, I can't access them directly anymore.

I tried two solutions which I will explain in the follow (+problems):

Solution 1: Provider

I used a Provider to keep track of a global Navbar State:

class NavbarState extends ChangeNotifier {
  bool _resizeBottom;

  NavbarState(this._resizeBottom);

  get resizeBottom => _resizeBottom;

  void setResizeBottom(bool state) {
    _resizeBottom = state;
    notifyListeners();
  }
}

Then In my Pages I set the state in the initState-Method with BlocProvider.of<NavbarState>(context).setResizeBottom(val) (respective for dispose).

This has 2 problems:

  1. Calling notifyListeners triggers a setState in the consumer and you can't call setState in the initState method.

  2. I have to declare this in every initState and dispose method.

Solution 2: Bloc

Once again I have a global state, but it does not have to inherit from `ChangeNotifier`. I track the state with a `NavbarBloc`-class.

Then I can add an event in the onGenerateRoute method. This is more handy then the provider approach, because there is just one place where I manage this state.

However, there is still a big problem:

When I navigate back, the onGenerateRoute Method does not get called and hence the state is not getting updated.

What the easiest solution would be

At least from an app-developer perspective it would be nice if I could just ask for the the current widget which is sitting in the active navigator.

Example of a Navbar

Here is an illustration of 3 navigators for the given cupertinotabscaffold. 3 Navigators while the 2nd is active

The middle "stack" is active and the topmost widget is seen on the screen. Thus, currently the resize param should be false. On navigating between the stacks (tapping navigation icon), the resize parameter should adjust. Furthermore, on navigating in between a single stack (push, pop) should also adjust the resize param (E.g. on a pop the param should be set to true).

I couldn't find anything like that. Thus I need your help.

2 Answers

For setting the state how about a setter as a callback to onTap?

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var values = [
    [true, false],
    [false, true],
  ];

  int stackIndex = 0;
  bool _resizeToAvoidBottomInsets = true;
  set _resizeToAvoidButtomInsets(bool value) => setState(() {
        print("set _resizeToAvoidBottomInsets = $value");
        _resizeToAvoidBottomInsets = value;
      });

  void handleTap(int i) {
    print("tapped: $i");
    _resizeToAvoidBottomInsets = values[stackIndex][i];
  }

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      resizeToAvoidBottomInset: _resizeToAvoidBottomInsets,
      child: CupertinoTabScaffold(
        tabBar: CupertinoTabBar(
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(icon: Icon(Icons.ac_unit)),
            BottomNavigationBarItem(icon: Icon(Icons.wb_sunny)),
          ],
          onTap: handleTap,
        ),
        tabBuilder: (BuildContext context, int index) {
          return CupertinoTabView(
            builder: (BuildContext context) {
              return CupertinoPageScaffold(
                navigationBar: CupertinoNavigationBar(
                  middle: Text('Page 1 of tab $index'),
                ),
                child: Center(
                  child: CupertinoButton(
                    child: Text(
                      'resizeToAvoidBottomInsets: $_resizeToAvoidBottomInsets',
                    ),
                    onPressed: () {
                      // set state and increment stack index before push
                      stackIndex++;
                      _resizeToAvoidButtomInsets = values[stackIndex][0];
                      Navigator.of(context).push(
                        CupertinoPageRoute<void>(
                          builder: (BuildContext context) {
                            return CupertinoPageScaffold(
                              navigationBar: CupertinoNavigationBar(
                                middle: Text('Page 2 of tab $index'),
                              ),
                              child: Center(
                                child: CupertinoButton(
                                    child: Text(
                                      'resizeToAvoidBottomInsets: $_resizeToAvoidBottomInsets',
                                    ),
                                    onPressed: () {
                                      // set state and decrement stack index before pop
                                      stackIndex--;
                                      _resizeToAvoidButtomInsets =
                                          values[stackIndex][0];
                                      Navigator.of(context).pop();
                                    }),
                              ),
                            );
                          },
                        ),
                      );
                    },
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

So I implemented a solution for the given problem. I don't think this is very smooth, but at least one can abstract the scaffold-properties from each single page.

I use a bloc for that as already mentioned in the question-solution-try-2.

The bloc emits a state MyScaffoldState which contains a ScaffoldProperties attribute. For this toy example I implemented it as follows:

class ScaffoldProperties {
  bool resizeBottom;
  String title;

  ScaffoldProperties({this.resizeBottom = false, required this.title});

  ScaffoldProperties copyWith({ScaffoldProperties? state}) {
    return ScaffoldProperties(
      resizeBottom: state?.resizeBottom ?? resizeBottom,
      title: state?.title ?? title,
    );
  }
}

You could add any scaffold-property to this class.

The main work happens in my MultiTabScaffold class. This class takes the parameters

List<Widget> pages;
List<BottomNavigationBarItem> items;
Route<dynamic>? Function(RouteSettings)? onGenerateRoute;
List<NavigatorObserver> navigatorObservers

And most importantly

Function<ScaffoldProperties> getScaffoldPropertiesFromRouteName(String? route);

The class wraps the necessary Scaffolds around the pages. Additionally, it uses a BlocBuilder and thus can use the ScaffoldProperties of the current state.

The getScaffoldPropertiesFromRouteName Method takes a route and returns a ScaffoldProperties-Instance. Thus all title, resizeProperties,... have to be collected here.

To update the current state (or emit events to the bloc), I had to modify a few points.

  1. onGenerateRoute: After generating the route (pushing to navigator), the ScaffoldProperties of the current page have to be emitted (with usage of the getScaffoldPropertiesFromRouteName Function)
  2. WillPopScope: Also after popping the page, I emit an event with the properties of the page below.
  3. Initial Value: Initially, when you open the app, neither onGenerateRoute nor willpop will be called. Thus, you have to have a different handling for the initial route '/' of every Navigator.

I have a toy example fully implemented.

This feels like a lot of work for such a simple use case, but I couldn't find a simpler way to do this. If there is one, please let me know.

Related