0

I want to use BottomNavigationBar of Flutter so for that I have created a class called BaseWidget which will be changed as the user taps the item.

class BaseWidget extends StatefulWidget {
  final String title;

  BaseWidget(this.title);

  _BaseWidgetState createState() => _BaseWidgetState(this.title);
}

class _BaseWidgetState extends State<BaseWidget> {
  final String title;

  _BaseWidgetState(this.title);

  @override
  Widget build(BuildContext context) {
    return Center(child: Text(title));
  }
}

In the above class am returning the Center widget with child as Text widget.

class HomeWidget extends StatefulWidget {
  _HomeWidgetState createState() => _HomeWidgetState();
}

class _HomeWidgetState extends State<HomeWidget> {
  int pageIndex = 0;

  final _home = BaseWidget('Home');
  final _business = BaseWidget('Business');
  final _school = BaseWidget('School');

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Bottom Navigation Bar'),
        ),
        body: choosePager(),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: pageIndex,
          items: <BottomNavigationBarItem>[
            BottomNavigationBarItem(
                icon: Icon(Icons.home), title: Text('Home')),
            BottomNavigationBarItem(
                icon: Icon(Icons.business), title: Text('Business')),
            BottomNavigationBarItem(
                icon: Icon(Icons.school), title: Text('School')),
          ],
          onTap: onTap,
        ),
      ),
    );
  }

  void onTap(int index) {
    setState(() {
      this.pageIndex = index;
    });
  }

  Widget choosePager() {
    switch (pageIndex) {
      case 0:
        return _home;
        break;
      case 1:
        return _business;
        break;
      case 2:
        return _school;
        break;
      default:
        return Text('Unknown');
        break;
    }
  }
}

Problem 1:

Whenever user taps on the BottomNavigationBarItem the text should change to the respected string passed in the BaseWidget's constructor. But it only shows Home and the rest 2 are ignored.

Problem 2:

I am planning to replace Center widget with the ListView widget to populate the list of Schools and Businesses which will be fetched from the network API in paginated way. So I don't want to reinitialise the classes again when BottomNavigationBarItem is tapped as that would result in loss of data which is already fetched. To prevent data lose I am declaring _home, _business & _school property and using these property in choosePager() method.

4
  • Did you check int index has the expected value every time when onTap(...) is called? Commented Apr 7, 2019 at 18:13
  • I think the issue is that body: choosePager(), is not rebuilt when you call setState. I'd suggest you move everything related to that feature to a new custom widget that you pass to body: ... instead of having it in _HomeWidgetState. Commented Apr 7, 2019 at 18:15
  • @GünterZöchbauer yes I did, indexes are correct. Commented Apr 7, 2019 at 18:21
  • choosePager() is rebuilt, but BaseWidgets other than _home are never built. Commented Apr 8, 2019 at 9:00

3 Answers 3

2

There are several issues with your code:

1- The real problem is that you never rebuild the BaseWidget. You construct 3 new BaseWidgets, but you only ever call the build of the _home widget, because it's the first one returned by choosePager(). Since you don't create _home, _business, _school in the HomeWidget build, no other BaseWidget can ever get built.

2- When you don't need to store any state/variables for a widget, use a Stateless widget.

3- Don't do anything in the constructor of your State. Use initState https://docs.flutter.io/flutter/widgets/State/initState.html for that instead.

4- Create widgets using const constructors when possible.

5- Widget constructor take named parameters. One of those should be the key. Use super to call the base constructor.

With that in mind, this is what the code should look like:


class BaseWidget extends StatelessWidget {
  final String title;

  const BaseWidget({Key key, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(title),
    );
  }
}

class HomeWidget extends StatefulWidget {
  _HomeWidgetState createState() => _HomeWidgetState();
}

class _HomeWidgetState extends State<HomeWidget> {
  int pageIndex = 0;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Bottom Navigation Bar'),
        ),
        body: choosePager(),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: pageIndex,
          items: <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              title: Text('Home'),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.business),
              title: Text('Business'),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.school),
              title: Text('School'),
            ),
          ],
          onTap: onTap,
        ),
      ),
    );
  }

  void onTap(int index) {
    setState(() {
      pageIndex = index;
    });
  }

  Widget choosePager() {
    Widget result;

    switch (pageIndex) {
      case 0:
        result = BaseWidget(title: 'Home');
        break;
      case 1:
        result = BaseWidget(title: 'Business');
        break;
      case 2:
        result = BaseWidget(title: 'School');
        break;
      default:
        result = Text('Unknown');
        break;
    }
    return result;
  }
}

Edit: For your example, you may want to fetch some data from the network and only use the widget to display it. In that case, create a new class (not a Widget) to fetch & hold on to the data, and use the Widget only for displaying the data.

Some sample code:

/// Silly class to fetch data
class DataClass {
  static int _nextDatum = 0;
  int _data;

  DataClass();

  Future<int> fetchData() async {
    await Future.delayed(Duration(
      milliseconds: 2000,
    ));
    _data = _nextDatum++;
    return _data;
  }

  int getData() {
    return _data;
  }
}

class BaseClass extends StatefulWidget {
  final String title;
  final DataClass data;

  const BaseClass({Key key, this.title, this.data}) : super(key: key);

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

class _BaseClassState extends State<BaseClass> {
  String title;
  DataClass data;

  @override
  Widget build(BuildContext context) {
    String dataStr = data == null ? ' - ' : '${data.getData()}';

    return Center(
      child: Text(
        '$title: $dataStr',
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    // initState gets called only ONCE
    title = widget.title;
    data = widget.data;
  }

  @override
  void didUpdateWidget(BaseClass oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.data != oldWidget.data) {
      data = widget.data;
    }
    if (widget.title != oldWidget.title) {
      title = widget.title;
    }
  }
}

class HomeWidget extends StatefulWidget {
  _HomeWidgetState createState() => _HomeWidgetState();
}

class _HomeWidgetState extends State<HomeWidget> {
  int pageIndex = 0;
  Map<String, DataClass> _dataMap = <String, DataClass>{};

  @override
  void initState() {
    super.initState();
    _init().then((result) {
      // Since we need to rebuild the widget with the resulting data,
      // make sure to use `setState`
      setState(() {
        _dataMap = result;
      });
    });
  }

  Future<Map<String, DataClass>> _init() async {
    // this fetches the data only once
    return <String, DataClass>{
      'home': DataClass()..fetchData(),
      'business': DataClass()..fetchData(),
      'school': DataClass()..fetchData(),
    };
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Bottom Navigation Bar'),
        ),
        body: choosePager(),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: pageIndex,
          items: <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              title: Text('Home'),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.business),
              title: Text('Business'),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.school),
              title: Text('School'),
            ),
          ],
          onTap: onTap,
        ),
      ),
    );
  }

  void onTap(int index) {
    setState(() {
      pageIndex = index;
    });
  }

  Widget choosePager() {
    Widget result;

    switch (pageIndex) {
     // it doesn't matter if you create a new BaseClass() a hundred times, flutter is optimized enough to not care. The `initState()` gets called only once.  You're fetching the data only once.
      case 0:
        result = BaseClass(
          title: 'Home',
          data: _dataMap['home'],
        );
        break;
      case 1:
        result = BaseClass(
          title: 'Business',
          data: _dataMap['business'],
        );
        break;
      case 2:
        result = BaseClass(
          title: 'School',
          data: _dataMap['school'],
        );
        break;
      default:
        result = Text('Unknown');
        break;
    }
    return result;
  }
}
Sign up to request clarification or add additional context in comments.

5 Comments

I don't want to reinitialise BaseWidget each time any BottomNavigationBarItem is tapped. Consider an example where in School Widget I am listing all the schools from network API and I switch to Home or Business Widget and then come back to School widget then all the list of schools I have fetched will be lost and will be fetched again since the class will be reinitialised.
@AnirudhaMahale flutter will not reinitialize things the way you are expecting, when you are dealing with Widgets. For your example, Flutter will not reinitialize things the way you're expecting. For you example, what you may want to do is have a separate class (not a widget) that fetches the data from the network and stores it. The widget only displays it. I will post an example in a minute.
Okay though I have noticed when I do it the way you have suggested, the init method is called every time I switch to new item.
What do you mean? Which init method are you calling?
BaseWidget's init method is called every time the tab is switched. Now I have not kept it in the snippet.
1

After lot of RND I solved my problem using IndexedStack. It shows the single Child from the list of children based on the index.

It will initialise all the children when the Widget build(BuildContext context) method of the _HomeWidgetState is called. So any time you switch the tabs, the object won't be reinitialised.

Here is my full code

class BaseWidget extends StatefulWidget {
  final String title;

  BaseWidget(this.title);

  _BaseWidgetState createState() => _BaseWidgetState();
}

class _BaseWidgetState extends State<BaseWidget> {
  @override
  Widget build(BuildContext context) {
    return Center(child: Text(widget.title));
  }
}

class HomeWidget extends StatefulWidget {
  _HomeWidgetState createState() => _HomeWidgetState();
}

class _HomeWidgetState extends State<HomeWidget> {
  int _pageIndex = 0;
  List<Widget> _children;

  @override
  void initState() {
    super.initState();

    _children = [
      BaseWidget('Home'),
      BaseWidget('Business'),
      BaseWidget('School')
    ];
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Bottom Navigation Bar'),
        ),
        body: IndexedStack(
          children: _children,
          index: _pageIndex,
        ),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: _pageIndex,
          items: <BottomNavigationBarItem>[
            BottomNavigationBarItem(
                icon: Icon(Icons.home), title: Text('Home')),
            BottomNavigationBarItem(
                icon: Icon(Icons.business), title: Text('Business')),
            BottomNavigationBarItem(
                icon: Icon(Icons.school), title: Text('School')),
          ],
          onTap: onTap,
        ),
      ),
    );
  }

  void onTap(int index) {
    setState(() {
      _pageIndex = index;
    });
  }
}

Comments

0

Please try this code, I have edited the BaseWidget class

  class BaseWidget extends StatefulWidget {
  final String title;

  BaseWidget(this.title);

  _BaseWidgetState createState() => _BaseWidgetState();
}

class _BaseWidgetState extends State<BaseWidget> {


  @override
  Widget build(BuildContext context) {
    return Center(child: Text(widget.title));
  }
}

3 Comments

Where this widget is defined in _BaseWidgetState ?
just replace your code block where you have defined BaseWidget and its state with the code i have provided. Replace first code block with my answer in your question
void initState() only called for the first tab i.e. Home.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.