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

Flutter全局多屏幕倒计时组件实现问题咨询

Hey there! Let's solve this cross-screen countdown problem you're dealing with—keeping it running smoothly as users navigate between screens is totally achievable, and it all comes down to decoupling your countdown logic from individual screen widgets so it doesn't get destroyed when pages change.

Here are two practical, battle-tested approaches to implement this:


Approach 1: Using ChangeNotifier + Provider (Flutter Official Recommendation)

This uses Flutter's built-in state management tools to keep your countdown logic alive across the entire app.

Step 1: Create a Global Countdown Service

First, make a reusable service class that handles the countdown logic and notifies listeners when the time changes:

import 'dart:async';
import 'package:flutter/foundation.dart';

class CountdownService extends ChangeNotifier {
  Timer? _timer;
  int _remainingSeconds = 0;
  bool _isRunning = false;

  // Expose state for widgets to access
  int get remainingSeconds => _remainingSeconds;
  bool get isRunning => _isRunning;

  // Start the countdown with a total duration in seconds
  void startCountdown(int totalSeconds) {
    if (_isRunning) return;
    
    _remainingSeconds = totalSeconds;
    _isRunning = true;
    notifyListeners(); // Tell widgets to update

    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (_remainingSeconds <= 0) {
        stopCountdown();
        return;
      }
      _remainingSeconds--;
      notifyListeners();
    });
  }

  // Stop and reset the timer
  void stopCountdown() {
    _timer?.cancel();
    _isRunning = false;
    notifyListeners();
  }

  // Reset to zero
  void resetCountdown() {
    stopCountdown();
    _remainingSeconds = 0;
    notifyListeners();
  }

  // Clean up timer when service is disposed
  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }
}

Step 2: Wrap Your App with Provider

In main.dart, wrap your entire app with ChangeNotifierProvider so every screen can access the service:

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

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CountdownService(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Cross-Screen Countdown',
      initialRoute: '/screen1',
      routes: {
        '/screen1': (context) => const Screen1(),
        '/screen2': (context) => const Screen2(),
      },
    );
  }
}

Step 3: Build Reusable Countdown Widget & Screens

Create a shared countdown display widget, then use it in any screen. The countdown will keep running even when you navigate:

// Reusable countdown display (use this in all screens)
class CountdownDisplay extends StatelessWidget {
  final CountdownService service;

  const CountdownDisplay({super.key, required this.service});

  String _formatTime(int seconds) {
    final minutes = (seconds ~/ 60).toString().padLeft(2, '0');
    final secs = (seconds % 60).toString().padLeft(2, '0');
    return '$minutes:$secs';
  }

  @override
  Widget build(BuildContext context) {
    return Text(
      _formatTime(service.remainingSeconds),
      style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
    );
  }
}

// Example Screen 1 (starts the countdown)
class Screen1 extends StatelessWidget {
  const Screen1({super.key});

  @override
  Widget build(BuildContext context) {
    final countdownService = Provider.of<CountdownService>(context);
    return Scaffold(
      appBar: AppBar(title: const Text('Screen 1')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CountdownDisplay(service: countdownService),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => countdownService.startCountdown(60), // 60-second countdown
              child: const Text('Start Countdown'),
            ),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: () => Navigator.pushNamed(context, '/screen2'),
              child: const Text('Go to Screen 2'),
            ),
          ],
        ),
      ),
    );
  }
}

// Example Screen 2 (countdown continues here)
class Screen2 extends StatelessWidget {
  const Screen2({super.key});

  @override
  Widget build(BuildContext context) {
    final countdownService = Provider.of<CountdownService>(context);
    return Scaffold(
      appBar: AppBar(title: const Text('Screen 2')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CountdownDisplay(service: countdownService),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => countdownService.stopCountdown(),
              child: const Text('Stop Countdown'),
            ),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('Back to Screen 1'),
            ),
          ],
        ),
      ),
    );
  }
}

Approach 2: Using GetX (Simpler, Less Boilerplate)

If you prefer a more lightweight solution with less code, GetX is a great option—it handles state management and dependency injection out of the box.

Step 1: Create a Countdown Controller

import 'dart:async';
import 'package:get/get.dart';

class CountdownController extends GetxController {
  final RxInt remainingSeconds = 0.obs; // Observable state
  final RxBool isRunning = false.obs;
  Timer? _timer;

  void startCountdown(int totalSeconds) {
    if (isRunning.value) return;
    
    remainingSeconds.value = totalSeconds;
    isRunning.value = true;

    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (remainingSeconds.value <= 0) {
        stopCountdown();
        return;
      }
      remainingSeconds.value--;
    });
  }

  void stopCountdown() {
    _timer?.cancel();
    isRunning.value = false;
  }

  void resetCountdown() {
    stopCountdown();
    remainingSeconds.value = 0;
  }

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

Step 2: Register the Controller Globally

In main.dart, register the controller so it's accessible anywhere:

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

void main() {
  Get.put(CountdownController()); // Global registration
  runApp(const MyApp());
}

// ... rest of MyApp code stays similar

Step 3: Use the Controller in Screens

class Screen1 extends StatelessWidget {
  const Screen1({super.key});
  final countdownController = Get.find<CountdownController>(); // Get the controller

  String _formatTime(int seconds) {
    final minutes = (seconds ~/ 60).toString().padLeft(2, '0');
    final secs = (seconds % 60).toString().padLeft(2, '0');
    return '$minutes:$secs';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Screen 1')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Use Obx to auto-update when state changes
            Obx(() => Text(
                  _formatTime(countdownController.remainingSeconds.value),
                  style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
                )),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => countdownController.startCountdown(60),
              child: const Text('Start Countdown'),
            ),
            ElevatedButton(
              onPressed: () => Get.to(() => const Screen2()),
              child: const Text('Go to Screen 2'),
            ),
          ],
        ),
      ),
    );
  }
}

Your Screen 2 will work the same way—just get the controller with Get.find() and use Obx to display the time.


Key Notes to Avoid Issues

  • Always cancel the Timer in the dispose/onClose method to prevent memory leaks.
  • If you need the countdown to survive app backgrounding, add logic to save the remaining time to shared_preferences and restore it when the app resumes.
  • Never put countdown logic inside a screen's State class—when the screen is popped, the state is destroyed, and the countdown stops.

内容的提问来源于stack exchange,提问作者Basit Abbasi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 18:52:48