作ったもの
import 'dart:async';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
Timer? _timer;
DateTime _time = DateTime.utc(0).add(const Duration(seconds: 10));
late DateTime _backgroundTime;
bool _isRunning = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_onForeground();
} else if (state == AppLifecycleState.paused) {
_onBackground();
}
}
void _onBackground() {
if (_isRunning) {
_timer?.cancel();
_isRunning = false;
_backgroundTime = DateTime.now();
}
}
void _onForeground() {
if (!_isRunning) {
_time = _time.subtract(DateTime.now().difference(_backgroundTime));
_checkTimeIsOver();
_startTimer();
}
}
void _checkTimeIsOver() {
if (!_time.isAfter(DateTime.utc(0))) {
_timer?.cancel();
_time = DateTime.utc(0);
_isRunning = false;
}
}
void _startTimer() {
if (_isRunning) return;
_isRunning = true;
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
setState(() {
_time = _time.add(const Duration(seconds: -1));
_checkTimeIsOver();
});
});
}
void _pauseTimer() {
_timer?.cancel();
_isRunning = false;
}
void _resetTimer() {
_timer?.cancel();
_isRunning = false;
setState(() => _time = DateTime.utc(0).add(const Duration(seconds: 10)));
}
@override
void dispose() {
_timer?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
final remaining = '${_time.minute.toString().padLeft(2, '0')}:${_time.second.toString().padLeft(2, '0')}';
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('残り $remaining', style: const TextStyle(fontSize: 24)),
const SizedBox(height: 24),
ElevatedButton(onPressed: _startTimer, child: const Text('Start Timer')),
ElevatedButton(onPressed: _pauseTimer, child: const Text('Pause Timer')),
ElevatedButton(onPressed: _resetTimer, child: const Text('Reset Timer')),
],
),
),
),
),
);
}
}
順番としては、
- Startする機能を作った
- Stopする機能を作った
- Pauseする機能を作った
- Background -> Foreground への遷移処理を書いた
と言った流れです。
単純な実装じゃダメな理由
モバイルアプリでは、アプリ本体が画面上にない間は、アプリ自体の動作はストップします。
要は、画面更新などが全く行われなくなるということです。
これで何が困るのかというと、タイマー見たいなリアルタイム処理が必要な場合に困ります。
バックグラウンド処理を書けばいいのでは?
と思った方、鋭いですが惜しいです。
まずバックグラウンド処理は、制約がクッソ厳しくて、難しいです。
また、タイマーなどはユーザーに表示すること自体に、意味がありますから、画面上の状態を管理して、制御する必要がありますが、バックグラウンド処理では、基本的に表示されていない画面上の状態を更新することはできません。
ただし、再送処理などは、バックグラウンドで行う必要がありますから、それらはまた別のお話です。
タイマーのための解決方法
ことタイマーに限って言えば、簡単な解決方法があります。
その方法は、バックグラウンドに行ったタイミングの時間を記録して、戻ってきたタイミングの時間と引き算、経過時間分だけ残タイマーから引くという処理です。
要は、タイマー自体は動かなくても、経過時間を知れるから、結果的にどれだけタイマーが進んでいなきゃおかしいかを知ることができる、という手法です。
これを実現するためには、バックグラウンドに入ったこととフォアグラウンドに戻ってきたということを検知しなければなりません。
そのために必要なのが、WidgetBindingObserverです。
これは、FlutterのWidget自体の表示状態を管理しているクラスです。
このクラスをMixinし、画面遷移のハンドラーであるdidChangeAppLifecycleState()をオーバーライドすれば、やっと実現できます。
didChangeAppLifecycleStateで検知できるStateは何?
大切なのは、2つ。
AppLifecycleState.pause
これは、フォアグラウンドで実行されていたアプリケーションが、バックグラウンドへ遷移した後の状態を示している。
AppLifecycleState.resume
これは、バックグラウンドにいたアプリケーションが、フォアグランドへ戻ってきた後の状態を示している。
これらの状態が変更した際に、このdidChangeAppLifecycleStateメソッドは、コールバックされる。
つまり何が言いたいの?
結論としては、
-
didChangeAppLifecycleStateメソッドを使って - アプリの状態変化を検知して
- バックグラウンドからフォアグラウンドに戻るまでの経過時間を検出して
- それをタイマーに反映する
- ことによって、バックグラウンドにいる間でも擬似的に動き続けるタイメーが作れる
という話でした。
コメントお待ちしてまーす!!