You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter应用进入后台时重置倒计时定时器?

问题:Flutter应用进入后台时自动重置倒计时定时器

我希望实现Flutter应用进入后台时自动重置倒计时定时器。目前已能检测App Lifecycle的不同状态,但无法在检测到AppLifecycleState.paused状态时触发定时器重置。

应用生命周期检测器位于根Widget TimerApp中,其home页为倒计时定时器页面TimerPage(如此设置是为了让生命周期检测覆盖整个应用,但不确定是否合理)。我原本想在case AppLifecycleState.paused中调用controller.reset,但由于controller定义在TimerPage中,无法在根Widget中直接使用。

根Widget代码如下:

class TimerApp extends StatefulWidget {
  const TimerApp({super.key});

  @override
  State<TimerApp> createState() => _TimerAppState();
}

class _TimerAppState extends State<TimerApp> with WidgetsBindingObserver {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.amber,
      ),
      home: const TimerPage(),
    );
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    switch (state) {
      case AppLifecycleState.paused:
        debugPrint('Paused State');
        break;
      case AppLifecycleState.resumed:
        debugPrint('Resumed State');
        break;
      case AppLifecycleState.inactive:
        debugPrint('Incactive State');
        break;
      case AppLifecycleState.detached:
        debugPrint('Detached State');
        break;
    }
  }
}

倒计时定时器页面代码如下:

class TimerPage extends StatefulWidget {
  const TimerPage({super.key});

  @override
  State<TimerPage> createState() => _TimerPageState();
}

class _TimerPageState extends State<TimerPage> with TickerProviderStateMixin {
  late AnimationController controller;

  bool isPlaying = false;

  String get countText {
    Duration count = controller.duration! * controller.value;
    return controller.isDismissed
        ? '${controller.duration!.inHours}:${(controller.duration!.inMinutes % 60).toString().padLeft(2, '0')}:${(controller.duration!.inSeconds % 60).toString().padLeft(2, '0')}'
        : '${count.inHours}:${(count.inMinutes % 60).toString().padLeft(2, '0')}:${(count.inSeconds % 60).toString().padLeft(2, '0')}';
  }

  double progress = 1.0;
  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 90),
    );
    controller.addListener(
      () {
        if (controller.isAnimating) {
          setState(
            () {
              progress = controller.value;
            },
          );
        } else {
          setState(
            () {
              progress = 1.0;
              isPlaying = false;
            },
          );
        }
      },
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            onPressed: () {
              debugPrint('invert colors');
            },
            icon: const Icon(Icons.invert_colors),
          ),
        ],
      ),
      body: Container(
        color: const Color.fromARGB(255, 152, 57, 57),
        height: 800,
        child: Column(
          children: [
            Expanded(
              child: Center(
                child: Stack(
                  alignment: Alignment.center,
                  children: [
                    SizedBox(
                      width: 300,
                      height: 300,
                      child: CircularProgressIndicator(
                        backgroundColor: Colors.grey,
                        value: progress,
                        strokeWidth: 8,
                      ),
                    ),
                    GestureDetector(
                      onTap: () {
                        if (controller.isDismissed) {
                          showModalBottomSheet(
                            context: context,
                            builder: (context) => SizedBox(
                              height: 300,
                              child: CupertinoTimerPicker(
                                initialTimerDuration: controller.duration!,
                                onTimerDurationChanged: (time) {
                                  setState(
                                    () {
                                      controller.duration = time;
                                    },
                                  );
                                },
                              ),
                            ),
                          );
                        }
                      },
                      child: AnimatedBuilder(
                        animation: controller,
                        builder: (context, child) => Text(
                          countText,
                          style: const TextStyle(
                              fontSize: 60, fontWeight: FontWeight.bold),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
              child:
                  Row(mainAxisAlignment: MainAxisAlignment.center, children: [
                GestureDetector(
                  onTap: () {
                    if (controller.isAnimating) {
                      controller.stop();
                      setState(() {
                        isPlaying = false;
                      });
                    } else {
                      controller.reverse(
                          from: controller.value == 0 ? 1.0 : controller.value);
                      setState(
                        () {
                          isPlaying = true;
                        },
                      );
                    }
                  },
                  child: RoundButton(
                      icon: isPlaying == true
                          ? Icons.pause_circle_outline_sharp
                          : Icons.play_circle_outline_sharp),
                ),
                GestureDetector(
                    onTap: () {
                      controller.reset();
                      setState(
                        () {
                          isPlaying = false;
                        },
                      );
                    },
                    child: const RoundButton(icon: Icons.stop_circle_outlined)),
              ]),
            ),
          ],
        ),
      ),
    );
  }
}
解决方案

方法1:将生命周期监听移到TimerPage中

既然controller属于TimerPage,直接在TimerPage的State类中添加生命周期监听,逻辑更内聚,可直接访问controller调用重置方法。

修改后的TimerPage代码:

class TimerPage extends StatefulWidget {
  const TimerPage({super.key});

  @override
  State<TimerPage> createState() => _TimerPageState();
}

class _TimerPageState extends State<TimerPage> with TickerProviderStateMixin, WidgetsBindingObserver {
  late AnimationController controller;

  bool isPlaying = false;

  String get countText {
    Duration count = controller.duration! * controller.value;
    return controller.isDismissed
        ? '${controller.duration!.inHours}:${(controller.duration!.inMinutes % 60).toString().padLeft(2, '0')}:${(controller.duration!.inSeconds % 60).toString().padLeft(2, '0')}'
        : '${count.inHours}:${(count.inMinutes % 60).toString().padLeft(2, '0')}:${(count.inSeconds % 60).toString().padLeft(2, '0')}';
  }

  double progress = 1.0;
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 90),
    );
    controller.addListener(
      () {
        if (controller.isAnimating) {
          setState(
            () {
              progress = controller.value;
            },
          );
        } else {
          setState(
            () {
              progress = 1.0;
              isPlaying = false;
            },
          );
        }
      },
    );
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    controller.dispose();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    if (state == AppLifecycleState.paused) {
      if (mounted) {
        controller.reset();
        setState(() {
          isPlaying = false;
          progress = 1.0;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    // 保留原build代码不变
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            onPressed: () {
              debugPrint('invert colors');
            },
            icon: const Icon(Icons.invert_colors),
          ),
        ],
      ),
      body: Container(
        color: const Color.fromARGB(255, 152, 57, 57),
        height: 800,
        child: Column(
          children: [
            Expanded(
              child: Center(
                child: Stack(
                  alignment: Alignment.center,
                  children: [
                    SizedBox(
                      width: 300,
                      height: 300,
                      child: CircularProgressIndicator(
                        backgroundColor: Colors.grey,
                        value: progress,
                        strokeWidth: 8,
                      ),
                    ),
                    GestureDetector(
                      onTap: () {
                        if (controller.isDismissed) {
                          showModalBottomSheet(
                            context: context,
                            builder: (context) => SizedBox(
                              height: 300,
                              child: CupertinoTimerPicker(
                                initialTimerDuration: controller.duration!,
                                onTimerDurationChanged: (time) {
                                  setState(
                                    () {
                                      controller.duration = time;
                                    },
                                  );
                                },
                              ),
                            ),
                          );
                        }
                      },
                      child: AnimatedBuilder(
                        animation: controller,
                        builder: (context, child) => Text(
                          countText,
                          style: const TextStyle(
                              fontSize: 60, fontWeight: FontWeight.bold),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
              child:
                  Row(mainAxisAlignment: MainAxisAlignment.center, children: [
                GestureDetector(
                  onTap: () {
                    if (controller.isAnimating) {
                      controller.stop();
                      setState(() {
                        isPlaying = false;
                      });
                    } else {
                      controller.reverse(
                          from: controller.value == 0 ? 1.0 : controller.value);
                      setState(
                        () {
                          isPlaying = true;
                        },
                      );
                    }
                  },
                  child: RoundButton(
                      icon: isPlaying == true
                          ? Icons.pause_circle_outline_sharp
                          : Icons.play_circle_outline_sharp),
                ),
                GestureDetector(
                    onTap: () {
                      controller.reset();
                      setState(
                        () {
                          isPlaying = false;
                        },
                      );
                    },
                    child: const RoundButton(icon: Icons.stop_circle_outlined)),
              ]),
            ),
          ],
        ),
      ),
    );
  }
}

同时根Widget TimerApp可移除生命周期监听:

class TimerApp extends StatefulWidget {
  const TimerApp({super.key});

  @override
  State<TimerApp> createState() => _TimerAppState();
}

class _TimerAppState extends State<TimerApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.amber,
      ),
      home: const TimerPage(),
    );
  }
}

方法2:使用GlobalKey访问TimerPage的State

若需在根Widget中处理生命周期,可通过GlobalKey获取TimerPage的State实例,调用其内部方法。

修改根Widget TimerApp:

class TimerApp extends StatefulWidget {
  const TimerApp({super.key});

  @override
  State<TimerApp> createState() => _TimerAppState();
}

class _TimerAppState extends State<TimerApp> with WidgetsBindingObserver {
  final GlobalKey<_TimerPageState> _timerPageKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.amber,
      ),
      home: TimerPage(key: _timerPageKey),
    );
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    if (state == AppLifecycleState.paused) {
      debugPrint('Paused State');
      _timerPageKey.currentState?.resetTimer();
    }
  }
}

在TimerPage的State中添加公开的重置方法:

class TimerPage extends StatefulWidget {
  const TimerPage({super.key});

  @override
  State<TimerPage> createState() => _TimerPageState();
}

class _TimerPageState extends State<TimerPage> with TickerProviderStateMixin {
  late AnimationController controller;

  bool isPlaying = false;

  void resetTimer() {
    if (mounted) {
      controller.reset();
      setState(() {
        isPlaying = false;
        progress = 1.0;
      });
    }
  }

  String get countText {
    Duration count = controller.duration! * controller.value;
    return controller.isDismissed
        ? '${controller.duration!.inHours}:${(controller.duration!.inMinutes % 60).toString().padLeft(2, '0')}:${(controller.duration!.inSeconds % 60).toString().padLeft(2, '0')}'
        : '${count.inHours}:${(count.inMinutes % 60).toString().padLeft(2, '0')}:${(count.inSeconds % 60).toString().padLeft(2, '0')}';
  }

  double progress = 1.0;
  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 90),
    );
    controller.addListener(
      () {
        if (controller.isAnimating) {
          setState(
            () {
              progress = controller.value;
            },
          );
        } else {
          setState(
            () {
              progress = 1.0;
              isPlaying = false;
            },
          );
        }
      },
    );
  }

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

  @override
  Widget build(BuildContext context) {
    // 保留原build代码不变
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            onPressed: () {
              debugPrint('invert colors');
            },
            icon: const Icon(Icons.invert_colors),
          ),
        ],
      ),
      body: Container(
        color: const Color.fromARGB(255, 152, 57, 57),
        height: 800,
        child: Column(
          children: [
            Expanded(
              child: Center(
                child: Stack(
                  alignment: Alignment.center,
                  children: [
                    SizedBox(
                      width: 300,
                      height: 300,
                      child: CircularProgressIndicator(
                        backgroundColor: Colors.grey,
                        value: progress,
                        strokeWidth: 8,
                      ),
                    ),
                    GestureDetector(
                      onTap: () {
                        if (controller.isDismissed) {
                          showModalBottomSheet(
                            context: context,
                            builder: (context) => SizedBox(
                              height: 300,
                              child: CupertinoTimerPicker(
                                initialTimerDuration: controller.duration!,
                                onTimerDurationChanged: (time) {
                                  setState(
                                    () {
                                      controller.duration = time;
                                    },
                                  );
                                },
                              ),
                            ),
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 11:30:50