どうでもいいですが話ですが自分はもう一人暮らしを始めて2年半程になります。
一人暮らし始めて最初の頃は自由にその日の気分に合わせて自分で食べたい物を決められてめっちゃ最高じゃんと思っていたものですが今となっては、この毎食毎食何を食べるかを決めなければならない時間が非常に億劫で面倒になってきました。
みなさんにもそんな経験ありませんか?
「もう何でもいいから誰か今日の夕飯を決めてくれ」
そんな面倒くさがりなあなたと何より自分自身の為に、今回はFlutterで簡単なスロット風ルーレットアプリを作ってみたので紹介しつつ要所を解説した後最後に今日(執筆当時)の晩ごはんのメニューを決めようと思います。
もちろん夕飯限定ではなく汎用的に様々な要素を追加してルーレットで決められるように作ってあるので、
今日の服の色どうしよう..
今日は何の映画見ようか…
今日会社に行くかバックレるか悩む…
等決めかねている際に是非活用してみてください。
誰でもコピペしてすぐに動かせるように意識してファイル分割無しの単一ファイルでコードを書いたので、効率が悪かったり煩雑な記述に感じる人もいるとは思いますが、ミノガシテクダサイ。
前置き長くなりましたが早速順に解説していきます。
目次
アプリの概要
まず雑に今回開発したアプリの概要を記載していきます。
使い方の手順としては
- テキストフィールドにルーレットに登録したいデータを入力
- 「Add」ボタンを押すと画面下部にデータのチェックボックスが表示される
- ルーレットに追加したいデータを任意で選択する
- 右下の発火ボタンをタップしてルーレットを回す
- 再度右下をタップするとストップする
これだけです。シンプル。
環境
・Mac OS X 10.15.4
FlutterやDartのバージョンはこんな感じ。
$ flutter --version
Flutter 2.2.2 • channel stable • https://github.com/flutter/flutter.git
Framework • revision d79295af24 (4 months ago) • 2021-06-11 08:56:01 -0700
Engine • revision 91c9fc8fe0
Tools • Dart 2.13.3
全体のコード
全体のコード量としては200行程です。
少し長いですが、そのままコピペすれば動くはずなので是非実際に動かして遊んでみてくださいー。
GitHubにもpushしているのでpullしてきてもokです。
https://github.com/yuto90/roulette
ちなみにgit pullしてきたら「Target of URI doesn’t exist: ‘package:flutter/material.dart’.」みたいなエラーが出たら下記の記事を参考にしてみてください。
【Flutter】git cloneしてきたらTarget of URI doesn’t exist: ‘package:flutter/material.dart’…でアプリが動かない時の対処法
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 画面に表示する要素のインデックス番号を格納する用
int index = 0;
// ルーレットの起動有無フラグ
bool isStart = false;
// Timerオブジェクトを格納する用
var timer;
// ルーレットに選択肢として追加した要素を格納する用
List<String> elem = [];
// 要素にチェックが入っているかをboolで格納しておく用
List<bool> checkBox = [];
// チェックボックスで選択されている要素を格納する用
List<String> checkedElem = [];
// 画面上部に表示する要素を格納する用
String displayWord = 'Roulette';
// テキストフィールドにアクセスするためのコントローラー
TextEditingController addController = TextEditingController();
void startTimer() {
if (elem.length > 0 && checkedElem.length > 1) {
isStart = !isStart;
if (isStart) {
timer = Timer.periodic(Duration(milliseconds: 100), onTimer);
} else {
setState(() {
timer.cancel();
});
}
}
}
void onTimer(Timer timer) {
setState(() {
index++;
if (index > checkedElem.length - 1) {
index = 0;
}
displayWord = checkedElem[index];
});
}
void addElem() {
if (addController.text != '' && !isStart) {
setState(() {
elem.add(addController.text);
checkBox.add(true);
checkedElem.add(addController.text);
addController.text = '';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 4,
child: Container(
width: double.infinity,
color: Colors.blue,
child: Center(
child: Text(
displayWord,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 40,
color: Colors.black,
),
),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
flex: 1,
child: Icon(
Icons.playlist_add_outlined,
color: Colors.blue,
size: 40.0,
),
),
Expanded(
flex: 5,
child: TextField(
controller: addController,
decoration: InputDecoration(
hintText: 'input elem',
),
),
),
Expanded(
flex: 2,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(Colors.blue),
),
onPressed: () {
addElem();
},
child: Text('Add'),
),
),
],
),
Expanded(
flex: 7,
child: Container(
width: double.infinity,
color: Colors.blue[100],
child: Center(
child: Column(
children: [
Expanded(
flex: 1,
child: ListView.builder(
itemCount: checkBox.length,
itemBuilder: (BuildContext context, int index) {
return CheckboxListTile(
value: checkBox[index],
title: Text(
elem[index],
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
controlAffinity: ListTileControlAffinity.leading,
onChanged: (val) {
if (!isStart) {
setState(() {
checkBox[index] = val!;
if (val) {
checkedElem.add(elem[index]);
} else {
checkedElem.remove(elem[index]);
}
// チェックした選択肢を追加、削除した際にはRangeErrorを回避するために一旦結果表示をリセット
displayWord = 'Roulette';
});
}
},
);
},
),
),
],
),
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
child: isStart == true
? Icon(
Icons.whatshot,
color: Colors.pink,
)
: Icon(Icons.whatshot),
onPressed: () {
startTimer();
},
),
);
}
}
要点だけ解説
addElem()
void addElem() {
if (addController.text != '' && !isStart) {
setState(() {
elem.add(addController.text);
checkBox.add(true);
checkedElem.add(addController.text);
addController.text = '';
});
}
}
addElem関数はTextFieldを入力して「Add」ボタンを押した際に発火する関数で、下記の変数にそれぞれ文字列と真偽値を格納する役割があります。
// ルーレットに選択肢として追加した要素を格納する用
List<String> elem = [];
// 要素にチェックが入っているかをboolで格納しておく用
List<bool> checkBox = [];
格納した後、テキストフィールドに入力されていたデータのリセット処理をしてsetState関数によって画面を再度描画しています。これにより画面下部に追加したデータのチェックボックスが表示されます。
あとバリデーションとして
テキストフィールドに何も入力されていない かつ ルーレット起動フラグがfalseの場合
のみにデータの追加ができるようにif文を設けています。
CheckBoxListTileのonChanged
onChanged: (val) {
if (!isStart) {
setState(() {
checkBox[index] = val!;
if (val) {
checkedElem.add(elem[index]);
} else {
checkedElem.remove(elem[index]);
}
// チェックした選択肢を追加、削除した際にはRangeErrorを回避するために一旦結果表示をリセット
displayWord = 'Roulette';
});
}
},
今回はデータをCheckBoxListTileの一覧にして表示しようと思います。
要素の一覧表示をする際にはListViewを使用します。
ListViewに関しては解説記事に使い方をまとめているので参考にしてみてください。
【Flutter】ListViewの使い方【サンプルコードあり】
ListView.builderはitemCountで定義した数字の数だけreturnを返してくれます。
そうして生成されたチェックボックスはタップされるとまずバリデーションとしてルーレットが起動中かどうかをif文で判定します。
ルーレット起動中にデータを追加したり削除したりするとRangeErrorになる可能性があるのでそれの対策です。
その結果がfalse、つまり起動していない場合、選択したチェックボックス(checkBox[index])に今代入されている真偽値の反対の値を代入して、その結果によってif文でCheckedListにデータを追加したり、削除したりと分岐させています。
そしてこの際にもRange Errorを回避するために画面上部の表示はデフォルトの文字列にリセットしておきます。
それらの結果がsetState関数によって画面が再描画されてチェックマークが付いたり、外れたりの動きが実装できます。
startTimer()
void startTimer() {
if (elem.length > 0 && checkedElem.length > 1) {
isStart = !isStart;
if (isStart) {
timer = Timer.periodic(Duration(milliseconds: 100), onTimer);
} else {
setState(() {
timer.cancel();
});
}
}
}
startTimer関数は右下の起動ボタンをタップすると発火する関数で、チェックボックスのチェック状態やデータの数によってルーレットを起動するか、しないかを判断するための関数です。
はじめのバリデーションとして
データそのものの数が0より上 かつ チェックされているデータ数が1より上の時
のみルーレット起動フラグ(isStart)を変更できるようにしています。
そして次のif文でisStartがtrueの場合(ルーレットが起動していない時にボタンをタップした場合)、Timerオブジェクトによって0.1秒毎にこの後説明するonTimer関数を繰り返し実行してルーレットが起動されます。
falseだった場合(ルーレットが起動中にボタンをタップした場合)は繰り返し実行中のTimerオブジェクトをcancelメソッドで中断してルーレットをストップさせます。
onTimer()
void onTimer(Timer timer) {
setState(() {
index++;
if (index > checkedElem.length - 1) {
index = 0;
}
displayWord = checkedElem[index];
});
}
onTimer関数は先程のstartTimer関数から発火する実際に画面上部のルーレット部分の表示を変更している関数です。
発火する度にcheckedElemのindexの数字を1づつ足していき、setState関数で画面に表示するデータを切り替えていきます。
実際に夕食を決めてみた
ざっと解説した所でお腹が空いたので早速今晩のメニューを決めようと思います。
とりあえずパッと頭に浮かんだ食べたい物を追加していきます!
ラーメン!!!!!!!!!!!
というわけでアプリの思し召しのままにラーメンを食べに行きました。
めちゃくちゃ美味かったです。
飯テロをかました所で今回は以上になります。
これで1日にしなければならない判断の量を多少減らせたと思うので生産性爆上がり間違いなしです!
今回は必要最低限の機能しか実装していないので「もっと機能欲しい!」って人は自分でソースをいじって遊んでみるのも楽しいと思います。
動作検証は何度もやったつもりですが、バグケースがあった場合はそっと教えてもらえると嬉しいです!
おわり