Flutter Reworked question: Issue sharing states between widget with button and widget with countdown timer

110

You can copy paste run full code below
Step 1: You can put controller inside CountDownTimerState
Step 2: Use GlobalKey

 CountDownTimer(key: _key)

Step 3: Call function start() inside _CountDownTimerState with _key.currentState

goSelected: () {
  setState(() {
  ...
  _controller.reverse(from: 10.0); // start the countdown animation
  final _CountDownTimerState _state = _key.currentState;
  _state.start();

...

class _CountDownTimerState extends State<CountDownTimer>
    with TickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 7));
    super
        .initState(); // here I have some difference to Andrey's answer because I do not use Tween
  }
  ...
  void start() {
    setState(() {
      _controller.reverse(from: 1.0);
    });
  }

working demo

enter image description here

full code

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;

class MainPage extends StatefulWidget {
  MainPage({Key key, this.title}) : super(key: key);
  final String title;

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

class _MainPageState extends State<MainPage> with TickerProviderStateMixin {
  AnimationController _controller;
  var answer = "0", correctAnswer = true, result = 0;

  GlobalKey _key = GlobalKey();

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 7));
  }

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

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
        //navigationBar: CupertinoNavigationBar(),
        child: SafeArea(
      child: Container(
        color: Colors.blue,
        child: Column(children: <Widget>[
          CreateKeypad(
            // creates a keypad with a go button. when go is clicked, countdown shall start
            prevInput: int.parse((answer != null ? answer : "0")),
            updtedInput: (int val) {
              setState(() => answer = val.toString());
            },
            goSelected: () {
              setState(() {
                if (answer == result.toString()) {
                  correctAnswer = true;
                }
                /*final problem = createProblem();
                        result = problem.result;*/
              });

              print("go");
              _controller.reverse(from: 10.0); // start the countdown animation
              final _CountDownTimerState _state = _key.currentState;
              _state.start();

              /* Future.delayed(
                      const Duration(
                        milliseconds: 300,
                      ),
                      () => setState(() => correctAnswer = true));*/
            },
          ),
          Container(
              height: 400,
              width: 400,
              child: CountDownTimer(key: _key)), // show countdown timer
        ]),
      ),
    ));
  }
}

// CREATE KEYPAD - all keys but "1! and "go" removed
class CreateKeypad extends StatelessWidget {
  final int prevInput;
  final VoidCallback goSelected;
  final Function(int) updtedInput;
  CreateKeypad(
      {@required this.prevInput, @required this.updtedInput, this.goSelected});

  @override
  Widget build(BuildContext context) {
    return Row(children: <Widget>[
      Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(2.0),
            child: SizedBox(
              width: 80.0,
              height: 80.0,
              child: CupertinoButton(
                child:
                    Text("1", style: TextStyle(color: CupertinoColors.black)),
                onPressed: () {
                  updtedInput(1);
                },
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(2.0),
            child: SizedBox(
              width: 80.0,
              height: 80.0,
              child: CupertinoButton(
                child:
                    Text("Go!", style: TextStyle(color: CupertinoColors.black)),
                onPressed: () => goSelected(),
              ),
            ),
          ),
        ],
      ),
    ]);
  }
}

// CREATE COUNTDOWN https://medium.com/flutterdevs/creating-a-countdown-timer-using-animation-in-flutter-2d56d4f3f5f1
class CountDownTimer extends StatefulWidget {
  CountDownTimer({Key key}) : super(key: key);
  //final AnimationController _controller;

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

class _CountDownTimerState extends State<CountDownTimer>
    with TickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 7));
    super
        .initState(); // here I have some difference to Andrey's answer because I do not use Tween
  }

  String get timerString {
    Duration duration = _controller.duration * _controller.value;
    return '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}';
  }

  void start() {
    setState(() {
      _controller.reverse(from: 1.0);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      child: AnimatedBuilder(
        animation: _controller,
        builder: (BuildContext context, Widget child) {
          return CustomPaint(
              painter: CustomTimerPainter(
            // this draws a white donut and a red diminishing arc on top
            animation: _controller,
            backgroundColor: Colors.green,
            color: Colors.red,
          ));
        },
      ),
    );
  }
}

class CustomTimerPainter extends CustomPainter {
  CustomTimerPainter({
    this.animation,
    this.backgroundColor,
    this.color,
  }) : super(repaint: animation);

  final Animation<double> animation;
  final Color backgroundColor, color;

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = backgroundColor
      ..strokeWidth = 10.0
      ..strokeCap = StrokeCap.butt
      ..style = PaintingStyle.stroke;

    canvas.drawCircle(size.center(Offset.zero), size.width / 2.0, paint);
    paint.color = color;
    double progress = (1.0 - animation.value) * 2 * math.pi;
    //print("progress ${progress}");
    canvas.drawArc(Offset.zero & size, math.pi * 1.5, -progress, false, paint);
  }

  @override
  bool shouldRepaint(CustomTimerPainter old) {
    //print(animation.value);
    return true;
  }
}

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MainPage(),
    );
  }
}
Share:
110
w461
Author by

w461

Programmed C long time ago, did some minor VBA in between and currently I am trying to create some basic home schooling apps in flutter and/or swift - just in case...

Updated on December 23, 2022

Comments

  • w461
    w461 over 1 year

    I am trying since some days to connect an animated stateful child widget with a countdown timer to the parent stateful widget with the user interaction. I found this answer from Andrey on a similar question (using Tween which I do not) that already helped a lot, but I still don't get it to work. My assumption is, the child's initState could be the reason. The timer's code comes from here.

    I have removed quite some code including some referenced functions/classes. This should provide a clearer picture on the logic:

    1. In MainPageState I declare and init the _controller of the animation
    2. In MainPageState I call the stateless widget CreateKeypad hosting among others the "go" key
    3. When go is clicked, this event is returned to MainPageState and _controller.reverse(from: 1.0); executed
    4. In MainPageState I call the stateful widget CountDownTimer to render the timer
    5. In _CountDownTimerState I am not sure if my initState is correct
    6. In _CountDownTimerState I build the animation with CustomTimerPainter from the timer code source

    The animation shall render a white donut and a red, diminishing arc on top. However, I only see the white donut, not the red timer's arc. Any hint is highly appreciated.

    class MainPage extends StatefulWidget {
      MainPage({Key key, this.title}) : super(key: key);
      final String title;
    
      @override
      _MainPageState createState() => _MainPageState();
    }
    
    
    class _MainPageState extends State<MainPage> with TickerProviderStateMixin {
      AnimationController _controller;
      var answer="0", correctAnswer = true, result = 0;
    
      @override
      void initState() {
        super.initState();
        _controller = AnimationController(vsync: this, duration: Duration(seconds: 7));
      }
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return CupertinoPageScaffold(
            navigationBar: CupertinoNavigationBar(
            ),
            child: SafeArea(
              child: Container(
                child: Column(
                    children: <Widget>[
                      CreateKeypad( // creates a keypad with a go button. when go is clicked, countdown shall start
                        prevInput: int.parse((answer != null ? answer : "0")),
                        updtedInput: (int val) {
                          setState(() => answer = val.toString());
                        },
                        goSelected: () {
                          setState(() {
                            if (answer == result.toString()) {
                              correctAnswer = true;
                            }
                            final problem = createProblem();
                            result = problem.result;
                          });
                          _controller.reverse(from: 1.0); // start the countdown animation
                          Future.delayed(const Duration(milliseconds: 300,),
                                  () => setState(() => correctAnswer = true));
                        },
                      ),
                      CountDownTimer(_controller), // show countdown timer
                    ]
                ),
              ),
            )
        );
      }
    }
    
    // CREATE KEYPAD - all keys but "1! and "go" removed
    class CreateKeypad extends StatelessWidget {
      final int prevInput;
      final VoidCallback goSelected;
      final Function(int) updtedInput;
      CreateKeypad({@required this.prevInput, @required this.updtedInput, this.goSelected});
    
      @override
      Widget build(BuildContext context) {
        return Row(
            children: <Widget> [
              Column(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.all(2.0),
                    child: SizedBox(
                      width: 80.0, height: 80.0,
                      child: CupertinoButton(
                        child: Text("1", style: TextStyle(color: CupertinoColors.black)),
                        onPressed: () {
                          updtedInput(1);
                        },
                      ),
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.all(2.0),
                    child: SizedBox(
                      width: 80.0, height: 80.0,
                      child: CupertinoButton(
                        child: Text("Go!", style: TextStyle(color: CupertinoColors.black)),
                        onPressed: () => goSelected(),
                      ),
                    ),
                  ),
                ],
              ),
            ]
        );
      }
    }
    
    // CREATE COUNTDOWN https://medium.com/flutterdevs/creating-a-countdown-timer-using-animation-in-flutter-2d56d4f3f5f1
    class CountDownTimer extends StatefulWidget {
      CountDownTimer(this._controller);
      final AnimationController _controller;
    
      @override
      _CountDownTimerState createState() => _CountDownTimerState();
    }
    
    class _CountDownTimerState extends State<CountDownTimer> with TickerProviderStateMixin {
    
      @override
      void initState() {
        super.initState(); // here I have some difference to Andrey's answer because I do not use Tween
      }
    
      String get timerString {
        Duration duration = widget._controller.duration * widget._controller.value;
        return '${duration.inMinutes}:${(duration.inSeconds % 60)
            .toString()
            .padLeft(2, '0')}';
      }
    
      @override
      Widget build(BuildContext context) {
        return Container(
          child: AnimatedBuilder(
            animation: widget._controller,
            builder:
                (BuildContext context, Widget child) {
              return CustomPaint(
                  painter: CustomTimerPainter( // this draws a white donut and a red diminishing arc on top
                    animation: widget._controller,
                    backgroundColor: Colors.white,
                    color: Colors.red,
                  ));
            },
          ),
        );
      }
    }
    
  • w461
    w461 over 3 years
    Thank you so much for your efforts! While your demo works perfectly fine and meanwhile I have also found my last error in my adaption (I copy pasted the relevant lines from your code), it still doesn't work in my context. Still no red arc. Could you please explain your solution a bit to help me finding the issue?
  • w461
    w461 over 3 years
    In the MainPage state, you create a global key _key. Then, I guess, you copy the state of CountDownTimer into _state with final _CountDownTimerState _state = _key.currentState;. Then it appears that you can launch the function start within _CountDownTimerState from MainPageState with ` _state.start();` This functions sets within _CountDownTimerState the local animation controller _controller to reverse from 1. Is this the flow?
  • w461
    w461 over 3 years
    STUPID ME: Wrapping a container with width and height around CountDownTimer(key: _key) as you did, chunhunghan, did the trick. I guess, I painted my arc somewhere else in space... Everything fine now - at least when I finished positioning the arc