45

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?

Ruchit
  • 2,137
  • 1
  • 9
  • 24
codeinprogress
  • 3,193
  • 7
  • 43
  • 69

6 Answers6

71

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;
          });
        },
      ),
    );
  }
}


hfimy
  • 711
  • 5
  • 2
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(...);
  }
Abel Tilahun
  • 1,705
  • 1
  • 16
  • 25
Jonah Williams
  • 20,499
  • 6
  • 65
  • 53
  • 27
    added the super.build(context); before Scaffold but it did not work :( – codeinprogress Oct 27 '18 at 09:21
  • 2
    I already use PageView and AutomaticKeepAliveClientMixin. But I still has problem, when I navigate to new page using navigator.push and pop back again, my widget still reloaded again. After adding your solution, now it worked as exspected. Thankyou. :) – axunic Apr 11 '19 at 15:56
  • I applied the `super.build(context);` before returning the Scaffold on each of the tabs and this seems to work so far! – David Kobia Apr 26 '19 at 18:26
  • 3
    I can't get this to work... I have a WebView inside of a CustomScrollView and SliverList that keeps refreshing/rebuilding... I can't ask anymore questions, can anyone help?! – Chris Feb 20 '20 at 18:12
  • Did anyone got this fixed guys? I get a blank screen when navigating back from the child page that extends AutomaticKeepAliveClientMixin – Biswas Khayargoli Nov 27 '22 at 09:29
5

So I wrapped the tab pages in Stack widget and now it keeps the state of the form intact. I am not sure if this is the right way to go but it does the job.

Here is the code:

AddPatientView (parent)

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

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

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


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("New Patient Record"),
      ),
      body: new Stack(
        children: <Widget>[
          new Offstage(
            offstage: _currentIndex != 0,
            child: AddPatientInfo(savePatient),
          ),
          new Offstage(
            offstage: _currentIndex != 1,
            child: AddPatientImages(_imageFileList),
          )
        ],
      ),
      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;
          });
        },
      ),
    );
  }
}
codeinprogress
  • 3,193
  • 7
  • 43
  • 69
2

AutomaticKeepAliveMixin only usually for pageview/tabview/another view that's rebuild, which is preserved a state, that won't rebuild, obviously we need controller

0

put "bool get wantKeepAlive =>true " right after the subclass.

class _AddPatientInfoState extends State<AddPatientInfo> with AutomaticKeepAliveClientMixin<AddPatientInfo> {
 @override
  bool get wantKeepAlive => true;

othercode....
}
-2
class YouTubeWidget extends StatefulWidget {
  const YouTubeWidget({Key? key}) : super(key: key);
  @override
  _YouTubeWidgetState createState() => _YouTubeWidgetState();
}

class _YouTubeWidgetState extends State<YouTubeWidget> with 
AutomaticKeepAliveClientMixin {
  @override
Widget build(BuildContext context) {
    super.build(context);
    Size size = MediaQuery.of(context).size;
    return SingleChildScrollView(
      child: Column(
        children: [
          Notice(),
          Container(
            height: size.height * 0.9,
            child: InAppWebView(
              initialUrlRequest: URLRequest(url: 
Uri.parse("https://www.youtube.com/embed/NZov8TSiLV0")),
            ),
          ),
          SizedBox(
            height: size.height * 0.01,
          ),
          Container(
            height: size.height * 0.9,
            child: InAppWebView(
              initialUrlRequest: URLRequest(url: 
Uri.parse("https://www.youtube.com/embed/QWyAFNjq33s"),),
            ),
          ),
        ],
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Sep 30 '21 at 06:39