Question

Flutter: AutomaticKeepAliveClientMixin is not working with BottomNavigationBar

I have page called AddPatientView with BottomNavigationBar containing AddPatientInfo and AddPatientImages pages. All of these three are Stateful widgets.

By default the AddPatientInfo opens up which has bunch of TextFields (to enter patient info), in the AddPatientImages page the user can add Images.

The problem is if I fill the TextFields on AddPatientInfo then go to AddPatientImages and then go back, all the TextFields are empty. Rightfully so since the entire widget tree gets rebuild and I loose all my filled in data.

So I am implementing AutomaticKeepAliveClientMixin so the state is maintained even if the tab is changed. But it does not seems to work:

Here is my code:

AddPatientView (the parent)

class AddPatientView extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _AddPatientViewState();
  }
}

class _AddPatientViewState extends State<AddPatientView> {
  int _currentIndex = 0;
  List<Widget> _children;
  List<File> _imageFileList = new List<File>();

  @override
  void initState() {
    super.initState();
    _children = [
      AddPatientInfo(savePatient),
      AddPatientImages(_imageFileList)
    ];
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("New Patient Record"),
      ),
      body: _children[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        items: [
          BottomNavigationBarItem(icon: new Icon(Icons.create), title: new Text('Info')),
          BottomNavigationBarItem(icon: new Icon(Icons.camera_alt), title: new Text('Images')),
        ],
        onTap: (int index) {
          setState(() {
            _currentIndex = index;
          });
        },
      ),
    );
  }
}

AddPatientInfo

class AddPatientInfo extends StatefulWidget {
  final Function savePatient;

  AddPatientInfo(this.savePatient){
  }

  @override
  State<StatefulWidget> createState() {
    return _AddPatientInfoState();
  }
}

class _AddPatientInfoState extends State<AddPatientInfo> with AutomaticKeepAliveClientMixin<AddPatientInfo> {

  Function _savePatient;
  String _firstName, _lastName, _gender, _phone, _email, _diabetesMeds, _hypertensionMeds, _others;
  int _age, _diabetesYears, _hypertensionYears, _smokesPerDay, _smokerYears;
  bool _diabetes = false, _hypertension = false, _smoker = false, _chestPain = false,
      _cva = false, _ckd = false, _breathlessness = false,
      _syncope = false, _sweating = false, _sweatingFeet = false;

  List<String> _genderList = new List<String>();
  List<String> _yesNoList = new List<String>();
  List<File> _imageFileList = new List<File>();

  @override
  void initState() {
    super.initState();
    _savePatient = widget.savePatient;
    _genderList.addAll(['Male', 'Female', 'Other']);
    _yesNoList.addAll(['Yes', 'No']);
    _gender = _genderList.elementAt(0);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomPadding: false,
      body: Container(
        margin: EdgeInsets.all(10.0),
        child: Form(
          child: new ListView(
            children: <Widget>[
              TextField(
                decoration: InputDecoration(
                    labelText: 'Patient First Name',
                    labelStyle: TextStyle(
                        color: Colors.black
                    ),
                    hintText: 'Enter patients first name'
                ),
                onChanged: (String value) {
                  setState(() {
                    _firstName = value;
                  });
                },
              ),
              TextField(
                decoration: InputDecoration(
                    labelText: 'Patient Last Name',
                    labelStyle: TextStyle(
                        color: Colors.black
                    ),
                    hintText: 'Enter patients last name'
                ),
                onChanged: (String value) {
                  setState(() {
                    _lastName = value;
                  });
                },
              ),
              //other textfield widgets below
            ],
          ),
        )
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

What am I missing here? Is there a more elegant way to do maintain the data in the form?

 48  41682  48
1 Jan 1970

Solution

 76

If you want AutomaticKeepAliveClientMixin to continue working, please use PageView to wrap the body, the code should like this

class AddPatientView extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _AddPatientViewState();
  }
}

class _AddPatientViewState extends State<AddPatientView> {
  int _currentIndex = 0;
  List<Widget> _children;
  List<File> _imageFileList = new List<File>();

  // add
  final pageController = PageController();
  void onPageChanged(int index) {
    setState(() {
      _currentIndex = index;
    });
  }

  @override
  void initState() {
    super.initState();
    _children = [AddPatientInfo(savePatient), AddPatientImages(_imageFileList)];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("New Patient Record"),
      ),
      // here!!!
      body: PageView(
        children: _children,
        controller: pageController,
        onPageChanged: onPageChanged,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        items: [
          BottomNavigationBarItem(
              icon: new Icon(Icons.create), title: new Text('Info')),
          BottomNavigationBarItem(
              icon: new Icon(Icons.camera_alt), title: new Text('Images')),
        ],
        onTap: (int index) {
          // setState(() {
          //   _currentIndex = index;
          // });
          // update
          pageController.jumpToPage(index);
        },
      ),
    );
  }
}

but if you just want to keep page state, I suggest you use IndexedStack, it's very simple,no need AutomaticKeepAliveClientMixin, the code should like this

class AddPatientView extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _AddPatientViewState();
  }
}

class _AddPatientViewState extends State<AddPatientView> {
  int _currentIndex = 0;
  List<Widget> _children;
  List<File> _imageFileList = new List<File>();

  @override
  void initState() {
    super.initState();
    _children = [
      AddPatientInfo(savePatient),
      AddPatientImages(_imageFileList)
    ];
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("New Patient Record"),
      ),
      body: IndexedStack(
        index:_currentIndex,
        children:_children
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        items: [
          BottomNavigationBarItem(icon: new Icon(Icons.create), title: new Text('Info')),
          BottomNavigationBarItem(icon: new Icon(Icons.camera_alt), title: new Text('Images')),
        ],
        onTap: (int index) {
          setState(() {
            _currentIndex = index;
          });
        },
      ),
    );
  }
}


2019-03-05

Solution

 27

From the documentation on AutomaticKeepAliveClientMixin:

/// A mixin with convenience methods for clients of [AutomaticKeepAlive]. Used
/// with [State] subclasses.
///
/// Subclasses must implement [wantKeepAlive], and their [build] methods must
/// call `super.build` (the return value will always return null, and should be
/// ignored).

So in your example, before you return the Scaffold just call super.build:

  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(...);
  }
2018-10-27