4

I want to keep child widget state using GlobalKey after parent's state is changed. There is a workaround by using Opacity in order to solve the problem, but I wonder why GlobalKey doesn't work as expected in this scenario.

import 'dart:async';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Retrieve Text Input',
      home: MainScreen(),
    );
  }
}

class MainScreen extends StatefulWidget {
  @override
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  final _key = GlobalKey();
  bool _showTimer = true;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Title'),
          centerTitle: false,
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              TextButton(
                  onPressed: () => setState(() {
                        _showTimer = !_showTimer;
                      }),
                  child: Text('show/hide')),
              _showTimer ? TimerWidget(key: _key) : Container()
            ],
          ),
        ));
  }
}

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

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

const int TIME_REMINDING_SECONDS = 480;

class _TimerWidgetState extends State<TimerWidget> {
  Timer _timer;
  int _start = TIME_REMINDING_SECONDS;

  @override
  Widget build(BuildContext context) {
    return Text(
        '${(_start ~/ 60).toString().padLeft(2, '0')}:${(_start % 60).toString().padLeft(2, '0')}',
        style: TextStyle(
            color: _start > 10 ? Colors.amber : Colors.red, fontSize: 20));
  }

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

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  _startTimer() {
    const oneSec = const Duration(seconds: 1);
    _timer = new Timer.periodic(
      oneSec,
      (Timer timer) => setState(
        () {
          if (_start < 1) {
            timer.cancel();
          } else {
            _start = _start - 1;
          }
        },
      ),
    );
  }
}

You will see the timer restarts to initial value every times the parent's state is changed. I tried with the solutions here but didn't work.

thanhbinh84
  • 17,876
  • 6
  • 62
  • 69
  • It seems, every time we change the state, `TimerWidget` is starting from initState. because we aren't preserving any state here, I believe. – Md. Yeasin Sheikh Aug 25 '21 at 09:34
  • Yes, that's why I tried to use `GlobalKey` to preserve `TimerWidget` – thanhbinh84 Aug 26 '21 at 04:01
  • You could just wrap it with a Visibility, Offstage or opacity. Or just lift its state up to an inherited widget, provider, bloc etc – croxx5f Aug 30 '21 at 11:10
  • The problem occurs because you are removing `TimerWidget` form widget tree and insert it again you can either hide it instead of removing it or use state management solution package. – Mohammed Alfateh Aug 30 '21 at 20:43
  • yes, we have several alternative solutions for this, but I want to understand how the `GlobalKey` works – thanhbinh84 Sep 01 '21 at 01:17

3 Answers3

2

as an option you can skip GlobalKey and simple use Offstage widget

Offstage(offstage: !_showTimer, child: TimerWidget()),

another answer mentioned Visibility with maintainState parameter.
This is pointless because it uses Offstage under the hood.

Nagual
  • 1,576
  • 10
  • 17
  • 27
0

By Every time in the previous code every time the state changes it creates a new instance of timer so GlobalKey won't take effect there since its new instance.

Global keys uniquely identify elements. Global keys provide access to other objects that are associated with those elements, such as BuildContext. For StatefulWidgets, global keys also provide access to State. https://api.flutter.dev/flutter/widgets/GlobalKey-class.html

By the Above statement, the global key is used to access the state within the widgget.

So in your case when TimerWidget() switches it's disposed of its state and not gonna preserve that's why its timer getting reset every time you change state.

--- Update ---

Instead of _showTimer ? TimerWidget(key: _key) : Container() Use below code:

Visibility(
            visible: _showTimer,
            maintainState: true,
            child: page
          )

Here, maintain state is keeping the state of the widget.

Arul
  • 1,031
  • 13
  • 23
  • yes, we have several alternative solutions for this, but I want to understand how the `GlobalKey` works – thanhbinh84 Sep 01 '21 at 01:18
  • I get your point, i have updated the answer please look at it. – Arul Sep 02 '21 at 15:04
  • 1
    Thank for the answer, I tried your solution but it showed error `The instance member '_key' can't be accessed in an initializer`. Try to put inside `initState()`, the error's gone but still doesn't work. Could you try there and update with full source code. Thanks – thanhbinh84 Sep 03 '21 at 01:27
  • Try the example, and let me know if its works. – Arul Sep 03 '21 at 04:02
  • it works but the solution is similar to use `Opacity` as the tree doesn't change so the widget is not destroyed. I want to understand use `GlobalKey` affectedly – thanhbinh84 Sep 03 '21 at 06:34
  • Quite similar but it practically removes the widget by documentation `when it is not visible, the replacement child (typically a zero-sized box) is included instead.` so via maintainState flag we persist the state of the widget and via global key we can access state. – Arul Sep 06 '21 at 07:19
0

Update

The following code moves the scope of a globally unique key so that it will maintain its state while the app lives. When adding this key to an Offset widget, you can show/hide the timer while retaining its state. Without this step, the timer widget would continue to reset as the timer widget is removed and re-added to the rendering tree. I also added the late modifier to the state class _timer variable.

Removing the timer widget from the tree will normally call the dispose method; so one alternative is to use Offstage which is designed to temporarily remove widgets based on state. This seems to be precisely what you are attempting to do. However, the Visibility widget does this same behavior without having to maintain a Global Key (but your focus seemed to be on wanting to leverage a key). Note the other widgets discussed in Visibility notes may provide other alternatives.

Some important considerations:

Animations continue to run when using Offstage widget.

From the docs (on the Offstage widget):

A widget that lays the child out as if it was in the tree, but without painting anything, without making the child available for hit testing, and without taking any room in the parent.

Offstage children are still active: they can receive focus and have keyboard input directed to them.

Animations continue to run in offstage children, and therefore use battery and CPU time, regardless of whether the animations end up being visible.

Offstage can be used to measure the dimensions of a widget without bringing it on screen (yet). To hide a widget from view while it is not needed, prefer removing the widget from the tree entirely rather than keeping it alive in an Offstage subtree.

From the docs (on the Visibility widget):

By default, the visible property controls whether the child is included in the subtree or not; when it is not visible, the replacement child (typically a zero-sized box) is included instead.

A variety of flags can be used to tweak exactly how the child is hidden. (Changing the flags dynamically is discouraged, as it can cause the child subtree to be rebuilt, with any state in the subtree being discarded. Typically, only the visible flag is changed dynamically.)

These widgets provide some of the facets of this one:

Opacity, which can stop its child from being painted. Offstage, which can stop its child from being laid out or painted. TickerMode, which can stop its child from being animated. ExcludeSemantics, which can hide the child from accessibility tools. IgnorePointer, which can disable touch interactions with the child. Using this widget is not necessary to hide children. The simplest way to hide a child is just to not include it, or, if a child must be given (e.g. because the parent is a StatelessWidget) then to use SizedBox.shrink instead of the child that would otherwise be included.

import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

//create a key that will persist in app scope
var timerKey = GlobalKey();

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Retrieve Text Input',
      home: MainScreen(),
    );
  }
}

class MainScreen extends StatefulWidget {
  const MainScreen({Key? key}) : super(key: key);
  @override
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  bool _showTimer = true;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Title'),
          centerTitle: false,
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              TextButton(
                  onPressed: () => {
                        setState(() {
                          _showTimer = !_showTimer;
                        })
                      },
                  child: Text('show/hide')),
              //reuse the current timer logic to show/hide the time
              Offstage(
                offstage: _showTimer,
                child: TimerWidget(
                  key: (timerKey),
                ),
              )
            ],
          ),
        ));
  }
}

class TimerWidget extends StatefulWidget {
  const TimerWidget({Key? key}) : super(key: key);

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

const int TIME_REMINDING_SECONDS = 480;

class _TimerWidgetState extends State<TimerWidget> {
  late Timer _timer;
  int _start = TIME_REMINDING_SECONDS;

  @override
  Widget build(BuildContext context) {
    return Text(
        '${(_start ~/ 60).toString().padLeft(2, '0')}:${(_start % 60).toString().padLeft(2, '0')}',
        style: TextStyle(
            color: _start > 10 ? Colors.amber : Colors.red, fontSize: 20));
  }

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

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  _startTimer() {
    const oneSec = const Duration(seconds: 1);
    _timer = new Timer.periodic(
      oneSec,
      (Timer timer) => setState(
        () {
          if (_start < 1) {
            timer.cancel();
          } else {
            _start = _start - 1;
          }
        },
      ),
    );
  }
}

Nota Bene

Visibility does not require a key at all.

 Visibility(
   visible: _showTimer,
   maintainState: true,
   child: TimerWidget(),
 ),

Original

Review my related question here. You will want to ensure that a Unique Key is available to the parent widget before you start to use the child. My example is pretty in-depth; let me know if you have follow-up issues.

Tommie C.
  • 12,895
  • 5
  • 82
  • 100
  • It doesn't work, because your case is changing parent whereas my case is removing from parent and adding again. Could you try my code to see if you can fix, I simplified, just place it on your main app then you see the problem. Thanks – thanhbinh84 Sep 04 '21 at 00:51