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
Timerin 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_preferencesand restore it when the app resumes. - Never put countdown logic inside a screen's
Stateclass—when the screen is popped, the state is destroyed, and the countdown stops.
内容的提问来源于stack exchange,提问作者Basit Abbasi

