Jardo
Jardo

Reputation: 2093

Flutter: Persisting Page States

Even after reading this and this, I still can't seem to wrap my head around storing page states in Flutter.

I've built a sample app, which has a main page called MyHomePage and a second page called SecondPage. MyHomePage has a floating action button, which displays SecondPage via Navigator.push(...). The second page contains a text field with an assigned controller. I would like to preserve the text field's text after I close and reopen SecondPage.

I've tried all sorts of combinations with setting buckets, page states and keys (inspired by the links above), but I couldn't make it work.

Also I'd like to store the whole page state automatically - without the need to write/retrieve every single value manually (in case I have a lot of text fields on the page).

Here is my code:

import 'package:flutter/material.dart';

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

PageStorageKey mykey = new PageStorageKey("testkey");

class MyApp extends StatelessWidget {

  final PageStorageBucket _bucket = new PageStorageBucket();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PageStorage(
        bucket: _bucket,
        child: MyHomePage(),
      )
    );
  }
}

class MyHomePage extends StatefulWidget {

  MyHomePage({Key key}) : super(key: key);

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

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("State demo"),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _openSecondPage,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

  _openSecondPage() {
    Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondPage()));
  }

}

class SecondPage extends StatefulWidget {
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {

  final _aController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second page"),
      ),
      body: Center(
        child: TextField(
          controller: _aController,
          key: mykey,
          autofocus: true,
        ),
      )
    );
  }
}

EDIT:

Based on Ajay's answer, I was able to greatly simplify the working code. Turns out that in order to persist widget states manually, all you need is an instance of PageStorageBucket in combination with ValueKey instances.

Here are the modifications I did to Ajay's code:

Here is the resulting code (simplest working form):

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {

  final bucket = PageStorageBucket();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(bucket: bucket,),
    );
  }
}

class MyHomePage extends StatefulWidget {

  final PageStorageBucket bucket;

  const MyHomePage({Key key, this.bucket}) : super(key: key);

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("State demo"),
      ),
      body: Center(
        child: Column(
          children: <Widget>[],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _openSecondPage,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

  _openSecondPage() {
    Navigator.push(
        context, new MaterialPageRoute(builder: (context) => new SecondPage(bucket: widget.bucket,)));
  }
}

class SecondPage extends StatefulWidget {

  final PageStorageBucket bucket;

  const SecondPage({Key key, this.bucket}) : super(key: key);

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

class _SecondPageState extends State<SecondPage> {

  static const KEY_A = ValueKey("secondPage.A");

  final _aController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _aController.addListener(_updateValue);

    String value = widget.bucket.readState(context, identifier: KEY_A) ?? "";
    _aController.text = value;
  }

  _updateValue() {
    widget.bucket.writeState(context, _aController.text, identifier: KEY_A);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second page"),
      ),
      body: Center(
        child: TextField(
          controller: _aController,
          autofocus: true,
        ),
      ),
    );
  }
}

Upvotes: 0

Views: 1324

Answers (1)

Ajay
Ajay

Reputation: 16300

you need to read and write the state as well.

demo

Check out the below code.

Note: I have used after_layout to initialize the text controller.

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

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

PageStorageKey mykey = new PageStorageKey("testkey");
final PageStorageBucket _bucket = new PageStorageBucket();

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: PageStorage(
          bucket: _bucket,
          child: MyHomePage(),
        ));
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("State demo"),
      ),
      body: Center(
        child: Column(
          children: <Widget>[],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _openSecondPage,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

  _openSecondPage() {
    Navigator.push(
        context, new MaterialPageRoute(builder: (context) => new SecondPage()));
  }
}

class SecondPage extends StatefulWidget {
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage>
    with AfterLayoutMixin<SecondPage> {
  final _aController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _aController.addListener(_updateValue);
  }

  @override
  void afterFirstLayout(BuildContext context) {
    String value =
        _bucket.readState(context, identifier: ValueKey(mykey)) ?? "";
    print(value);
    _aController.text = value;
  }

  _updateValue() {
    _bucket.writeState(context, _aController.text, identifier: ValueKey(mykey));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second page"),
      ),
      body: Center(
        child: TextField(
          controller: _aController,
          key: mykey,
          autofocus: true,
        ),
      ),
    );
  }
}

Upvotes: 1

Related Questions