Flutter – Retaining current view state

eye-catch Dart and Flutter

When using PageView and moving to another page and coming back to the original page, its state is clear. I just wanted to check another page’s state but I have to input the value again. It’s not nice. How can we implement to retain the state?

Sponsored links

Structure of widget tree

My widget structure looks like the following.

The state of ItemSelectView is clear When switching the page. ItemWidget has one of the widgets for the input. It depends on the entries stored in a database in which items are shown on the page. I want to retain the value of the child widget of ItemWidget. Namely, they are CounterWidget and NumberInputWidget.

Sponsored links

Using PageStorage for widgets located at the bottom layer

Firstly, I tried to use PageStorageKey for the widgets located at the bottom layer. However, I’ve already added GlobalObjectKey to ItemWidget and specified it to the constructor of CounterWidget because ItemWidget needs to clear the value of the child widget. It means that I can’t add an additional key to the widget.
I tried the following code but it doesn’t work in my app because GlobalObjectKey is specified as the key in ItemWidget which is the parent widget.

class NumberInputWidget extends StatefulWidget {
  final double maxWidth;
  final bool showCounter;

  NumberInputWidget({
    this.maxWidth = 80,
    this.showCounter = false,
    Key? key,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() => _NumberInputWidget();
}

class _NumberInputWidget
    extends ValueControllerState<NumberInputWidget> {
  TextEditingController _controller = TextEditingController();
  String _text = "";

  @override
  void initState() {
    super.initState();
    String? storedValue = PageStorage.of(context)?.readState(context);
    if (storedValue != null) {
      _controller.text = storedValue;
    } else {
      _text = "";
    }
    _controller.addListener(() {
      _text = _controller.text;
      PageStorage.of(context)?.writeState(context, _text);
    });
  }

  @override
  void dispose() {
    super.dispose();
    this._controller.dispose();
  }
...
}

It tries to get the previous state but it is always null because the key is not PageStorageKey. In this case, writeState function in the TextEditingController’s listener doesn’t save the data. To save the data, we need PageStorageKey.

If this is used in a different place with PageStorageKey it should work.

// main.dart
var pageView = PageView(
    controller: _controller,
    children: [
    ItemSelectView(),
    // added new page that has input widget
    Scaffold(
        body: Center(
        child: NumberInputWidget(
            key: PageStorageKey<String>("KKK"),
        ),
    )),
    TimerView(),
    ],
);

It didn’t work!! Following is the error message.

════════ Exception caught by widgets library ═══════════════════════════════════
The following _CastError was thrown building MouseRegion(listeners: <none>, cursor: defer, state: _MouseRegionState#df326):
type 'String' is not a subtype of type 'double?' in type cast

The relevant error-causing widget was
TextField
lib\Widget\NumberInputWidget.dart:63
When the exception was thrown, this was the stack
#0      ScrollPosition.restoreScrollOffset
package:flutter/…/widgets/scroll_position.dart:420
#1      new ScrollPosition
package:flutter/…/widgets/scroll_position.dart:105
#2      new ScrollPositionWithSingleContext
package:flutter/…/widgets/scroll_position_with_single_context.dart:56
#3      ScrollController.createScrollPosition
package:flutter/…/widgets/scroll_controller.dart:234
#4      ScrollableState._updatePosition
package:flutter/…/widgets/scrollable.dart:422
...

It took me a while to find the wrong line. The following listener causes this error. _text holds string value but it seems that the value is handled as double value inside of writeState function.

_controller.addListener(() {
    _text = _controller.text;
    PageStorage.of(context)?.writeState(context, _text);
});

The problem has gone when I added the identifier.

@override
void initState() {
  super.initState();
  // id is specified to identifier arg
  final id = ValueKey("value");
  String? storedValue =
      // id is specified
      PageStorage.of(context)?.readState(context, identifier: id);
  if (storedValue != null) {
    _controller.text = storedValue;
  } else {
    _text = "";
  }
  _controller.addListener(() {
    _text = _controller.text;
    // id is specified
    PageStorage.of(context)?.writeState(context, _text, identifier: id);
  });
}

It retains its value even when switching the views. You might recognize that the same value is shown in a different view. This is because the same key "value" is used for a different instance. To fix the bug, we can use for example hasCode.

final id = ValueKey(widget.hashCode);

The new instance is created many times for State class but StatefulWidget class doesn’t change. Therefore, we should use widget hashCode instead of State class hashCode.

It might be better to define onChanged event in TextField than using TextEditingController in this simple case.

Setting PageStorageKey to parent widget

Let’s look at my app structure again.

widget-tree

In the previous example, PageView widget has NumberInputWidget directly. It means that it can assign PageStorageKey to the NumberInputWidget. However, ItemWidget has already assigned GlobalObjectKey to NumberInputWidget in my app. I couldn’t assign PageStorageKey to those input widgets in ItemWidget anymore.
But, wait… It is enough to retain the data in ItemSelectView. Following code does what I want without any other changes.
This is my misunderstanding. It looked working because the view showed correct contents when I switched the two views quickly.

var pageView = PageView(
  controller: _controller,
  children: [
    ItemSelectView(key:PageStorageKey("ItemSelectView")),
    TimerView(),
  ],
);

Comments

  1. Yair says:

    Thanks for the article.
    Is there a way to keep the pageView state itself when switching to another app page? My use case is with flutterflow. A feed page based on pageView that uses infinite loading. Now the user swiped down 20 times, click on “View profile” button and then clicked on “Return” button. He/she expects to be in the same swiping position and see the last post again.

    There is no built in way to preserve the list of read posts and the pageView index and infinite scrolling offset.

    Any idea how this problem may be solved?

    Thanks

    • Yuto Yuto says:

      Hi, Yair. Thank you for reading my article.

      The way of this article is not a popular way in Flutter. To keep the current state, I recommend that you should use Provider or Riverpod. I would use Riverpod.
      You can keep the content and the position in variables defined in Riverpod. You can use the stored value until you remove them in a specific action.

      I wrote some articles Riverpod. This article helps you consider the structure.
      Flutter How to implement Bloc like pattern with Riverpod

Copied title and URL