Flutterのストップウォッチクラス(Stopwatch)を使って激ムズな3秒ジャストで止めるストップウォッチアプリを作成してみます。
何が激ムズなのかというと、マイクロ秒まで3秒ジャストで止めるとクリアだからです!
難しすぎるのでクリアするための隠しコマンドを仕込んでます。
アプリの構造自体は簡単なので、「初学者だけど勉強がてら、簡単なアプリを作ってみたい!」方にもオススメです。
Stopwatchクラスとは
Flutterのストップウォッチクラス(Stopwatch)は、時間の計測やタイマー機能を簡単に実装できる便利なクラスです。
このクラスを使用することで、アプリケーションで経過時間を正確に測定したり、一定時間ごとにタスクを実行したりすることが可能になります。
Stopwatchクラスは、Flutterフレームワークに組み込まれているため、追加のパッケージのインストールは不要です。
タイマーやアニメーション、パフォーマンス測定など、さまざまな用途に活用することができます。
Stopwatchクラスの主な機能には以下が含まれます:
start()
メソッド:ストップウォッチの計測を開始します。stop()
メソッド:計測を停止します。start()
から呼び出された時間を計測します。reset()
メソッド:ストップウォッチの値をリセットし、計測を停止します。elapsed
プロパティ:現在の経過時間を取得します。isRunning
プロパティ:ストップウォッチが実行中かどうかを示す真偽値を取得します。
どんなアプリなのか?どこが激ムズなのか?
ジャスト3秒で止めるストップウォッチのアプリです。
ただのストップウォッチではなく、ミリ秒・マイクロ秒まで表示されたストップウォッチです!
ミリ秒ですら難しいのにマイクロ秒までを3秒ジャストで止めるなんて正直無理だと思います。
そのためになんと裏コマンドを用意しています。
それを使えば誰でも3秒ジャストで揃えられます。それは後で紹介します
実装方法
- Stopwatchクラスでストップウォッチの秒数を表示
- modelとviewにロジックを分離
- スタートボタンを実装
- ストップ/リセットボタンを実装
- チートボタンを作って勝手にジャスト3秒で止まるようにする
Stopwatchクラスでストップウォッチの秒数を表示
ストップウォッチクラスを使って、ストップウォッチを表示するようにします。
ストップウォッチインスタンスを定義し、それらの値をウィジェットロード時に実行可能なinitStateでelaspedにより取得します。
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: const MyApp(),
),
);
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Stopwatch _stopwatch = Stopwatch();
String seconds = "00";
String milliseconds = "000";
String microseconds = "000";
void _refresh() {
String str2Digits(int n) => n.toString().padLeft(2, "0");
String str3Digits(int n) => n.toString().padLeft(2, "0");
seconds = str2Digits(_stopwatch.elapsed.inSeconds.remainder(60));
milliseconds = str3Digits(_stopwatch.elapsed.inSeconds.remainder(60));
microseconds = str3Digits(_stopwatch.elapsed.inSeconds.remainder(60));
}
@override
void initState() {
super.initState();
_refresh();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Text(seconds + "." + milliseconds + "." + microseconds),
),
);
}
}
modelとviewにロジックを分離
statefulwidgetを使うとストップウォッチのロジック部分(例えば、スタート・ストップなど)
とストップウォッチの秒数やボタンを表示するビューのコードが一つのクラスの中で記述されることで
責任が複数の見にくいコードとなってしまいます。
そこでflutter_riverpodを使ってロジックを分離していきましょう
flutter_riverpodのインストールが必要です
flutter pub add flutter_riverpod
モデル側でストップウォッチの情報を保持し、ビュー側にはプロバイダーにより渡すことができます
model.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final stopwatchProvider = ChangeNotifierProvider((ref) => StopwatchModel());
class StopwatchModel extends ChangeNotifier {
Stopwatch _stopwatch = Stopwatch();
String seconds = "00";
String milliseconds = "000";
String microseconds = "000";
void _refresh() {
String str2Digits(int n) => n.toString().padLeft(2, "0");
String str3Digits(int n) => n.toString().padLeft(2, "0");
seconds = str2Digits(_stopwatch.elapsed.inSeconds.remainder(60));
milliseconds = str3Digits(_stopwatch.elapsed.inSeconds.remainder(60));
microseconds = str3Digits(_stopwatch.elapsed.inSeconds.remainder(60));
}
StopwatchModel() {
_refresh();
notifyListeners();
}
}
view.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tmp_blog/home/model.dart';
class Home extends ConsumerWidget {
const Home({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final StopwatchModel stopwatchModel = ref.watch(stopwatchProvider);
return Scaffold(
body: SafeArea(
child: Text(stopwatchModel.seconds +
"." +
stopwatchModel.milliseconds +
"." +
stopwatchModel.microseconds),
),
);
}
}
mainクラスがHomeクラスを呼び出すだけになります
main.dart(一部)
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return Home();
}
}
スタートボタンを実装
ストップウォッチの秒数をスタートさせるためのボタンを実装します。
ストップウォッチインスタンスのstartメソッドを使うことでインスタンス内の秒数のフィールドが更新されていきます。
それをperiodicによりミリ秒ごとにrefreshで画面の秒数を更新することで、
インスタンス内の秒数を画面に表示するようにします
model.dart(一部)
void start() {
_stopwatch.start();
Timer.periodic(const Duration(milliseconds: 1), (_) {
_refresh();
});
}
ビュー側ではモデルに定義されたスタートメソッドをボタン押下時の処理として呼び出します
view.dart(一部)
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
stopwatchModel.seconds +
"." +
stopwatchModel.milliseconds +
"." +
stopwatchModel.microseconds,
style: TextStyle(
fontSize: 56,
fontFeatures: [FontFeature.tabularFigures()],
),
),
ElevatedButton(
onPressed: () {
stopwatchModel.start();
},
child: Text("Start"),
),
],
),
ストップ/リセットボタンを実装
同じようにボタンの処理を実装します。
ただiPhoneのようにボタンは一つにしたいので、
ストップウォッチの状態によってボタンテキストの表示とボタン押下時の処理を変えるようにします。
また、ここでもストップウォッチのクラスとボタン状態のクラスを分けています。
この考え方も単一責任原則に基づいています
model.dart(一部)
enum Status { start, stop, reset }
class StopwatchModel extends ChangeNotifier {
Stopwatch _stopwatch = Stopwatch();
String seconds = "00";
String milliseconds = "000";
String microseconds = "000";
Status nextState = Status.start;
.
.
.
void doExecute() {
if (nextState == Status.start) {
_start();
} else if (nextState == Status.stop) {
_stop();
} else if (nextState == Status.reset) {
_reset();
}
}
void _start() {
_stopwatch.start();
Timer.periodic(const Duration(milliseconds: 1), (_) {
_refresh();
});
buttonContext.display(Status.start);
nextState = Status.stop;
}
void _stop() {
_stopwatch.stop();
buttonContext.display(Status.stop);
nextState = Status.reset;
}
void _reset() {
_stopwatch.reset();
buttonContext.display(Status.reset);
nextState = Status.start;
}
}
class ButtonContext {
String buttonDisplay = "start";
void display(Status status) {
if (status == Status.start) {
buttonDisplay = "stop";
} else if (status == Status.stop) {
buttonDisplay = "reset";
} else if (status == Status.reset) {
buttonDisplay = "start";
}
}
}
view.dart(一部)
ElevatedButton(
onPressed: () {
stopwatchModel.doExecute();
},
child: Text(stopwatchModel.buttonContext.buttonDisplay),
),
チートボタンを作って勝手にジャスト3秒で止まるようにする
ジャスト3秒で止まるような処理を内部に作ってしまうことでチート機能を作ってしまいます。
マイクロ秒までを3秒ジャストで止めるなんで無理です(笑)
これで自慢しちゃいましょう!!!
処理の記載自体はスタートボタンと同じような実装で、
最終的なコードは以下の通りです
ボタンを長く押下した時にチート機能が動作するようにしています
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tmp_blog/home/view.dart';
void main() {
runApp(
ProviderScope(
child: MaterialApp(
home: const MyApp(),
),
),
);
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return Home();
}
}
view.dart
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tmp_blog/home/model.dart';
class Home extends ConsumerWidget {
const Home({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final StopwatchModel stopwatchModel = ref.watch(stopwatchProvider);
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
stopwatchModel.seconds +
"." +
stopwatchModel.milliseconds +
"." +
stopwatchModel.microseconds,
style: TextStyle(
fontSize: 56,
fontFeatures: [FontFeature.tabularFigures()],
),
),
ElevatedButton(
onPressed: () {
stopwatchModel.doExecute();
},
onLongPress: () {
stopwatchModel.doCheat();
},
child: Text(stopwatchModel.buttonContext.buttonDisplay),
),
],
),
),
),
);
}
}
model.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final stopwatchProvider = ChangeNotifierProvider((ref) => StopwatchModel());
enum Status { start, stop, reset }
class StopwatchModel extends ChangeNotifier {
Stopwatch _stopwatch = Stopwatch();
String seconds = "00";
String milliseconds = "000";
String microseconds = "000";
Status nextState = Status.start;
ButtonContext buttonContext = ButtonContext();
void _refresh() {
String str2Digits(int n) => n.toString().padLeft(2, "0");
String str3Digits(int n) => n.toString().padLeft(3, "0");
seconds = str2Digits(_stopwatch.elapsed.inSeconds.remainder(60));
milliseconds =
str3Digits(_stopwatch.elapsed.inMilliseconds.remainder(1000));
microseconds =
str3Digits(_stopwatch.elapsed.inMicroseconds.remainder(1000));
notifyListeners();
}
StopwatchModel() {
_refresh();
}
void doExecute() {
if (nextState == Status.start) {
_start();
} else if (nextState == Status.stop) {
_stop();
} else if (nextState == Status.reset) {
_reset();
}
}
void doCheat() {
_stopwatch.start();
Timer.periodic(const Duration(milliseconds: 1), (_) {
_refresh();
if (_stopwatch.elapsed.inSeconds == 3) {
_stop();
_magic();
}
});
buttonContext.display(Status.start);
nextState = Status.stop;
}
void _magic() {
String str3Digits(int n) => n.toString().padLeft(3, "0");
seconds = str3Digits(3);
milliseconds = str3Digits(0);
microseconds = str3Digits(0);
notifyListeners();
}
void _start() {
_stopwatch.start();
Timer.periodic(const Duration(milliseconds: 1), (_) {
_refresh();
});
buttonContext.display(Status.start);
nextState = Status.stop;
}
void _stop() {
_stopwatch.stop();
buttonContext.display(Status.stop);
nextState = Status.reset;
}
void _reset() {
_stopwatch.reset();
buttonContext.display(Status.reset);
nextState = Status.start;
}
}
class ButtonContext {
String buttonDisplay = "start";
void display(Status status) {
if (status == Status.start) {
buttonDisplay = "stop";
} else if (status == Status.stop) {
buttonDisplay = "reset";
} else if (status == Status.reset) {
buttonDisplay = "start";
}
}
}
コメント