Search code examples
flutterstate-managementinherited-widget

Flutter: How Should I Use an InheritedWidget?


Introduction: I believe that this problem in principle might be shared, but I feel that this specific application is not. I humbly submit it for consideration, so please be gentle. I have stumbled into a clear trap here with my code, and I simply don't know how to fix it. After thinking about it for hours, I don't even have an inkling of what to do.

Problem: The InheritedWidget is not updating the state in the widget above it.

Expectation: I would expect that when I swipe on the PageView of the HomePage, the Text of the BottomAppBar will update. However, this does not happen.

Description: I am trying to use an InheritedWidget (InheritedPageIndex), which I initialised above the root widget (MaterialApp) via a Provider method (please see the code below). Which, has a value that passes down to the widget below it (MenuPage) in the widget tree. I want this value to update when the state changes; which I attempt modifying by swiping a PageView (HomePage) in the widget below it in the Widget tree.

enter image description here

Code: I have re-written and simplified my code so we can more easily diagnose and fix the problem, it looks like this:

page_index_model.dart

import 'package:flutter/material.dart';

    class PageIndex {
      PageIndex({@required this.index});
      double index = 0;
    }

page_index_provider.dart

import 'package:flutter/cupertino.dart';
import 'package:testinginheritedwidget/models/page_index_model.dart';

class InheritedPageIndex extends InheritedWidget {
  InheritedPageIndex({Key key, @required this.indexData, Widget child})
      : super(key: key, child: child);

  final PageIndex indexData;

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => true;

  static PageIndex of(BuildContext context) => context
      .dependOnInheritedWidgetOfExactType<InheritedPageIndex>()
      .indexData;
}

main.dart

import 'package:flutter/material.dart';
import 'package:testinginheritedwidget/models/page_index_model.dart';
import 'package:testinginheritedwidget/pages/menu_page.dart';
import 'package:testinginheritedwidget/providers/page_index_provider.dart';

    void main() => runApp(App());

    class App extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return InheritedPageIndex(
          indexData: PageIndex(index: 0),
          child: MaterialApp(
            debugShowCheckedModeBanner: false,
            title: 'Testing Inherited Widgets',
            home: MenuPage(),
          ),
        );
      }
    }

menu_page.dart

    import 'package:flutter/material.dart';
    import 'package:testinginheritedwidget/models/page_index_model.dart';
    import 'package:testinginheritedwidget/pages/home_page.dart';
    import 'package:testinginheritedwidget/providers/page_index_provider.dart';

    class MenuPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        PageIndex _pageIndex = InheritedPageIndex.of(context);

        return DefaultTabController(
          length: 3,
          child: Scaffold(
            //TODO: dot indicator
            resizeToAvoidBottomInset: false,
            appBar: AppBar(
              backgroundColor: Colors.black,
              title: Center(
                child: Text('Testing Inherited Widgets',
                    style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold)),
              ),
              bottom: TabBar(
                onTap: (tabIndex) {},
                labelColor: Colors.white,
                indicatorColor: Colors.white,
                tabs: const <Widget>[
                  Tab(icon: Icon(Icons.flight), child: Text('Flight')),
                  Tab(icon: Icon(Icons.map), child: Text('Map')),
                  Tab(icon: Icon(Icons.print), child: Text('Print')),
                ],
              ),
            ),
            body: SafeArea(
              child: TabBarView(
                children: <Widget>[
                  HomePage(),
                  HomePage(),
                  HomePage(),
                ],
              ),
            ),
            bottomNavigationBar: BottomAppBar(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
// *** PROBLEM 1 IS HERE ***
                  Text('${_pageIndex.index}'),
                ],
              ),
            ),
          ),
        );
      }
    }

home_page.dart

    import 'package:flutter/material.dart';
    import 'package:testinginheritedwidget/models/page_index_model.dart';
    import 'package:testinginheritedwidget/providers/page_index_provider.dart';

    class HomePage extends StatefulWidget {
      @override
      _HomePageState createState() => _HomePageState();
    }

    class _HomePageState extends State<HomePage> {
      List<Widget> cards = [];

      @override
      Widget build(BuildContext context) {
        PageIndex _pageIndex = InheritedPageIndex.of(context);

        for (var index = 0; index < 7; index++) {
          cards.add(
            Center(
              child: Text(
                '$index',
                style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
              ),
            ),
          );
          print(index);
        }
        return PageView(
          onPageChanged: (index) {
            setState(() {
//*** PROBLEM 2 IS HERE ***
              _pageIndex.index = index.toDouble();
              print('page index: ${_pageIndex.index}');
            });
          },
          children: cards,
        );
      }
    }

Assumption: After much research and reflection. I believe that I cannot update the value of the Provider; because it is between the first instantiated InheritedWidget and the method that changes the state. Maybe another way of saying it is that the State that I want to change is directly above it; and, it instead takes the second Provider's state instead of using the first's?

Questions:

1) For my benefit, am I correct in my assumption, or is there something else going on here?

2) How can I fix my problem? I don't believe that implementing a BLoC method would solve this because it too uses a provider?

I don't know how I would pass the state up the widget tree in this case without the InheritedWidget.


Solution

  • A Solution With Packages

    Introduction: For the benefit of thoroughness, I have included the code as the final answer which uses the Provider package, as suggested in the comments above. This code, as you would expect, is simpler than the above answer which uses no packages.

    The code:

    main.dart

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import 'package:stackoverflowquestionthree/counter_model.dart';
    import 'package:stackoverflowquestionthree/page_menu.dart';
    
    void main() => runApp(App());
    
    class App extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Testing Inherited Widgets',
          home: ChangeNotifierProvider(
            create: (BuildContext context) => Counter(0),
            child: MenuPage(),
          ),
        );
      }
    }
    

    counter_model.dart

    import 'package:flutter/material.dart';
    
    class Counter with ChangeNotifier {
      int _counter;
    
      Counter(this._counter);
    
      getCounter() => _counter;
    
      setCounter(int counter) => _counter = counter;
    
      void increment(int counter) {
        _counter = counter;
        notifyListeners();
      }
    }
    

    page_menu.dart

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import 'package:stackoverflowquestionthree/counter_model.dart';
    import 'package:stackoverflowquestionthree/home_page.dart';
    
    class MenuPage extends StatefulWidget {
      @override
      _MenuPageState createState() => _MenuPageState();
    }
    
    class _MenuPageState extends State<MenuPage> {
      var _bloc;
    
      @override
      Widget build(BuildContext context) {
        final counter = Provider.of<Counter>(context);
    
        return DefaultTabController(
          length: 3,
          child: Scaffold(
            //TODO: dot indicator
            resizeToAvoidBottomInset: false,
            appBar: AppBar(
              backgroundColor: Colors.black,
              title: Center(
                child: Text('Testing Inherited Widgets',
                    style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold)),
              ),
              bottom: TabBar(
                onTap: (tabIndex) {
                  counter.increment(0);
                },
                labelColor: Colors.white,
                indicatorColor: Colors.white,
                tabs: const <Widget>[
                  Tab(icon: Icon(Icons.flight), child: Text('Flight')),
                  Tab(icon: Icon(Icons.map), child: Text('Map')),
                  Tab(icon: Icon(Icons.print), child: Text('Print')),
                ],
              ),
            ),
            body: SafeArea(
              child: TabBarView(
                children: <Widget>[
                  HomePage(),
                  HomePage(),
                  HomePage(),
                ],
              ),
            ),
            bottomNavigationBar: BottomAppBar(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  //TODO: insert value below.
                  Text(counter.getCounter().toString()),
                ],
              ),
            ),
          ),
        );
      }
    }
    

    home_page.dart

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import 'package:stackoverflowquestionthree/counter_model.dart';
    
    class HomePage extends StatefulWidget {
      @override
      _HomePageState createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
      List<Widget> cards = [];
    
      @override
      Widget build(BuildContext context) {
        final counter = Provider.of<Counter>(context);
    
        for (var index = 0; index < 7; index++) {
          cards.add(
            Center(
              child: Text(
                '$index',
                style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
              ),
            ),
          );
          print(index);
        }
        return PageView(
          onPageChanged: (index) {
            setState(() {
              counter.increment(index);
            });
          },
          children: cards,
        );
      }
    }