アプリメモ
アプリを作成する目的
- アプリを作成することを通してまだ習熟度が低いFlutterの習熟度を上げるため
- アプリの作成からストアへの出品までのプロセスを体験することで、アプリを出す一連の流れについての知見を獲得する
コンセプト
- todo管理 + ポモドーロ + コンシェルジュ
todo管理
- 期限と見積もり時間をやることとセットで管理できる
ポモドーロ
- todoをやっている際の時間を計測できる
コンシェルジュ
- 使える時間を指定することで、おすすめのtodoを提案してくれる
どんな機能を実装するのか
例:工数タイマー
- タスク管理
- プロジェクト・タスクの作成・編集・削除
- タスクの見積もり工数の設定
- タスクを行うのにかかった工数・開始日・終了日を自動で記録
- タイマー機能
- 行うタスクを指定してタイマーを動かす
- タイマーは一時停止、停止することができ、経過時間をタスクに自動で記録
- タイマーを0分に戻す
- その他
- お問い合わせ
- プライバシーポリシー
- ライセンス
- バージョン表記
アプリを出すことを最優先にしたいため、このアプリのMVPを考える必要がある
ちょっと調べてみたら、やることを自動で提案してくれるようなtodoアプリはなかったので、それをMVPに据えたアプリにしたい
- todo管理
- 名前、見積もり、優先度、期日を指定してtodoを作成する
- todoは編集・削除できる
- todoの完了を記録できるようにする
- ポモドーロ
- 基本的なポモドーロに限定する
- 「25minスプリント + 5min休憩」のサイクルを繰り返す
- todoの見積もりを25分間隔でしか指定できないようにする
- 基本的なポモドーロに限定する
- コンシェルジュ
- タスクをやるのに使える時間をユーザに入力させ、その時間で最適なtodoを提案する
コンシェルジュのアルゴリズム
- 「タスクをやるのに使える時間 = 提案するtodoの見積もり時間の合計」 を満たすtodoの組み合わせの中で以下の条件を両方満たす組み合わせを選択する。
- 選択されていないtodoの中で期日が現在より古いtodoの数を最小にする
- 完了できるtodoの数を最大にする
制限
- タスクをやるのに使える時間は30分間隔でしか指定できないようにする
- todoは25分単位で完了できるようにする(進捗率25%みたいなイメージ)
-
タスクをやるのに使える時間 = todoの見積もり
を満たすtodoが存在する場合
- そのタスクを選択して終了
-
タスクをやるのに使える時間 = todoの見積もり
を満たすtodoが存在しない場合
-
タスクをやるのに使える時間 > todoの見積もり
を満たすtodoが存在する場合- 条件を満たすtodoの中で期日が現在の日付から最も近いtodoを選択する
- 条件を満たすtodoが複数個存在した場合は、「見積もり - 着手した時間」がより小さい方を選択する
- それでも複数個存在する場合は、優先度がより高い方を選択する
- それでも複数個存在する場合は、todoを登録した日時がより古い方を選択する
- タスクをやるのに使える時間から選択したタスクの見積もりを引いた値を、タスクをやるのに使える時間として1に戻る
-
タスクをやるのに使える時間 > todoの見積もり
を満たすtodoが存在しない場合- 期日が現在の日付から最も近いtodoを選択する
- 条件を満たすtodoが複数個存在した場合は、「見積もり - 着手した時間」がより小さい方を選択する
- それでも複数個存在する場合は、優先度がより高い方を選択する
- それでも複数個存在する場合は、todoを登録した日時がより古い方を選択する
- 終了
アプリの初回起動時に、todoで何を重要視するかを聞いてみて、それをアルゴリズムの判定に活用するといいかも
- パーソナライズしたものには愛着が湧くためそれを利用できる(出典は忘れた)
完了できるタスクの数を最大化したいんなら、このアルゴリズムは不適ではないのか?
-
タスクをやるのに使える時間 = todoの見積もりを満たすtodoが存在する場合それを選ぶ
ようにすると、以下のような状況では目的を達成できなくなる
タスクをやるのに使える時間:120min
タスク候補:タスクA(120min)、タスクB・C・D・E(30min)
選択:タスクA(120min)
完了できたタスク数:1
完了できるタスクの最大値:4
-
タスクをやるのに使える時間 = 選択したtodoの見積もりの合計を満たす
については部分和問題で判定可能 -
上の条件を満たす組み合わせはナップサック問題が適用できる
- ナップサック問題が適用できるかどうかは後で検討する
- 多分できなさそうな気がする
-
タスクをやるのに使える時間 = 選択したtodoの見積もりの合計
を満たす組み合わせが存在するかどうかは部分和問題数え上げ問題を適用して判定できる -
判定のパターンが以下の三種
組み合わせが存在する(単体)
組み合わせが存在する(複数)
組み合わせが存在しない
- 組み合わせが複数個存在する場合は、組み合わせの中で個数が最大になる組み合わせを
最小個数部分和問題
を改変して対応できる?
参考
https://qiita.com/drken/items/a5e6fe22863b7992efdb#問題-5最小個数部分和問題
懸念
- 部分和問題によってtodoの組み合わせを選択すると、todoの見積もりしか判定に使われない。
- 優先度や期日も判定に加えたいがどうしたらよいか
- 部分和問題では条件を満たす組み合わせが存在するかどうかしかわからないため、組み合わせそのものをどうにかして取得しなくてはいけない
コンシェルジュ機能の暫定仕様
- 期日が本日から3日以内のtodo(Aグループ)とそれ以外のtodo(Bグループ)に分けて考える。
- Aグループのtodoは期日が若い順に可能な限りすべて選択する
- Aグループのtodoをすべて選択してもタスクに使える時間が残っている場合は、以下の基準のどちらかに従ってBグループからtodoを選択する
- 期日までの日数が少ないtodoを選択する
- 選択したtodoの数が最大になるように選択する
- 選択するアルゴリズムは最小個数部分和問題を利用
- どちらの基準を使うのかは、アプリの初回起動時または設定からユーザに選ばせる
PDCA + todo
コンセプト
タスクの見積もり時間をPDCAを通して改善していく
ターゲット
- タスクを行うのにどれだけかかるかを正確に見積もりできるようになりたい新卒一年目の会社員
ターゲットが抱えている課題感
PDCAの各フェーズでやる必要があること
P
- タスクにかかる時間を見積もる
D
- タスクをやる
- タスクが完了するまでの時間を測る
C
- 見積もった時間と実際にタスクをやるのにかかった時間を比較する
A
- 差が生じた理由を考える
PDCAの各フェーズで抱えている課題感
P
- タスクにかかる時間を見積もる際に前回のActionの内容を思い出す必要がある
- 何かしらの記録媒体にActionの内容を保存したとしてもそれを参照しなければならない
- 前回のタスクと今回のタスクでやっている種類が違う場合、そのActionを活かせない場合がある
- Actionにはどんなタスクに対しても言えるようなものと特定のタスクの時だけしか適用できないものがある
D
- 複数のタスクを並行してやっている際に、時間を計測するのが大変
- タイマーのつけ忘れ、止め忘れなど
- 手動でかかった時間を記録している場合は、計算が面倒
C
- 見積もりの予実の差を手動で算出するのが面倒
A
- 差が生じた理由を考える際にテンプレートがあると考えやすいかも
抱えている課題感に対するアプローチ
P
- Actionにタグをつけて、どういったタイプのTask、TodoにはこういったActionがありましたみたいなことを提案する
D
- タイマーを開始する際にタスクを選択させることで、アプリ側で自動でかかった時間を記録する
C
- 見積もりの予実の差の計算はアプリ側で自動で記録したい
A
- テンプレートについてはあったらいいな程度(まだ解決策が思い浮かばない)
実装する機能
- Taskの登録・編集・削除
- 1つのTaskに複数個のTodoを登録できる
- 名前が登録できる
- Taskに登録するTodoを追加・削除できる
- Taskを削除したらそれに属するTodoをすべて削除する
- Todoの登録・編集・削除
- 名前と見積もりを登録できる
- 名前と見積もりを編集できる
- Todoを削除できる
- Notice(Actioinで出てきた振り返り)の登録・編集
- Task・Todoが完了したタイミングでNoticeを登録する画面を出す
- Noticeにはtagをつける
- Task・Todo1つにつき1つのNoticeを登録できる
- 1つのNoticeには複数個のtagをつけることができる
- 一度登録したNoticeを編集することができる
- Notice表示機能
- 見積もりを行う際にそのTask・Todoのtagに該当するNoticeを表示する
- いくつかだけ画面に表示して、それ以上は別画面で検索機能を提供する
- Noticeが存在する場合は「過去のTaskで考えたNoticeを活用してみませんか?」みたいな文章を出してみたい
- TaskやTodoを削除する際にそれらに紐付いたNoticeも消すか消さないか聞くようにする
Task
- taskId(String, PK)
- name(String)
- statusId(int, FK)
Todo
- todoId(int, PK)
- taskId(int, FK)
- name(String)
- statusId(int, FK)
- estimate(int): 分単位で保持
- elapsed(int): 分単位で保持
Status
- statusId(int, PK)
- name(String)
TaskNotice
- taskNoticeId(int, PK)
- taskId(int, FK)
- body(String)
TodoNotice
- todoNoticeId(int, PK)
- todoId(int, FK)
- body(String)
Tag
- tagId(int, PK)
- name(String)
TaskNoticeTag
- taskNoticeId(int, PK)
- tagId(int, PK)
TodoNoticeTag
- todoNoticeId(int, PK)
- tagId(int, PK)
- Task:Todo = 1:n
- Task:TaskNotice = 1:1
- Task:Status = n:1,
- Todo:TodoNotice = 1:1
- Todo:Status = n:1,
- TaskNotice:Tag = n:m
- TodoNotice:Tag = n:m
- タグはどうつけよう?
- iPhone標準のメモはメモの中に#~~でタグを好きなだけ残せるからそれに従う?
- hashtagableでタグの部分のフォントスタイルを変更できる
- flutter_social_textfieldでタグのリコメンドをリアルタイムに行えそう
flutter_social_textfieldのsampleを動かしてみたが、あらかじめ用意してあるものにしか#hashtagのリコメンドが表示されない
新しく追加したhashtagもリコメンドで2回目以降に出してほしい
tag専用の入力フィールドという限定ならflutter_taggingというパッケージが使えそう
iPhone標準のメモのタグの挙動について
- 文字数制限はなさそうだが、20文字以上の長さになると、はじめの10文字と終わりの10文字だけ表示され残りは...になる
- タグのリコメンデーションはキーボード直上に2つだけ横にならんで表示される
- 1つしかないときはキーボード直上中央に表示される
pubdevで調べてみたけど単一のパッケージで対応できているものはなかったので組み合わせ前提で考えたほうがよさそう
- #hashtagを入力したら、hashtagだけべつのfontStyleにする
- #hashtagの候補を表示する
- 新しく追加したhashtagも2回目以降入力する際には候補の中に含まれるようにする
- タグの入力欄とNoticeの入力欄を別にするならflutter_tagging使えばすべて解決できる
- iPhone標準のメモの使い勝手が良すぎるからこれを再現したい
- Noticeへのタグ付けは必須機能ではあるが、付ける方法は妥協してもいいのでは?
- Flutter Taggingのsampleコードが動かないで困ってる
- hashtagableでどうにかできないか見てみる
参考
- どうにもならなそう
- flutter_social_textfieldのここ でリコメンドするhashtagを出すようにしているから、ここの表示にflutter_taggingを組み合わせられないかやってみる
- まずはflutter_taggingの動作確認ができるコードを書くところからやる。
- 20:00までには終わらせたい
最小限のコードどころかimport 'package:flutter_tagging/flutter_tagging.dart';
しただけでエラー吐くから
これ使うならこのバグ修正からやらないとヤバそう
- リポジトリを見ていたら同じ現象に対して言及しているissueがあったためこれを読んで見る
- 問題を解決するPRは出ているがマージがされていない
- マージされるまでは以下のようにすればうまくいくそうだ
flutter_tagging:
git:
url: https://github.com/mejdi14/flutter_tagging
- 本家はnull safetyに対応していないためnull safetyに対応した方をつかうように変更
こいつはflutter_typeahead: 3.2.1
(バージョン固定)に依存しているのでバージョンを指定してpub getする必要あり
- とりあえず動かせるようにはなった
- でもこれ新しいタグが2回目以降にリコメンドされてこない
- これは仕様?それともfork元と先で仕様が違う?それとも変な書き方をしているせい?
- 入力に対してリコメンドしてくれる文言が変わるのはいいが、なんかもっさりしてる
- シミュレータ固有の問題か?(実機ならもうちょい早く動く?)
アプリ名はImploop(インプループ)
-
PDCAを回して改善していくことからImprove + Loopの造語
-
AppStoreとGooglePalayStoreで同じ名前を持つアプリはないから問題無し
TaskとTodoの登録処理から着手
- これを参考にする
- NoSQLは正直ぜんぜん理解できていないため今回はRelationalを採用する
https://docs.flutter.dev/cookbook/persistence/sqlite
複数個のテーブルを作る際には以下の方法でできそう
UIもへったくれもないけど、Taskの一覧表示とTaskの新規追加を画面でできるになった
このタイミングでテストをぶち込みたい
sqflite公式が用意しているテスト用のモックを使ってみるsqflite_common_ffi
テストは後回しにしてまずアプリの完成を目指す。
正直敗北感がすごい
は?更新されないんだが?
formKeyが更新されないだけやった
その前の段階やった
そもそもformFieldにkeyを割り当ててなかった
カフェで開発すんのはNOT FOR MEなキガス
freezedが欲しくなってきた
- freezedで作成したクラスにメソッドを追加する場合は、以下の手順を踏む
- withからimplementsにかえる
- デフォルトのコンストラクタを潰す
2つめ以降のフォームの現在値がとれない
型の不一致が原因やった
GlobalKey<FormFieldState>のcurrentStateがnullになるのは、以下の条件のいずれかにあてはまるとき
- there is no widget in the tree that matches this global key
- that widget is not a [StatefulWidget], or the associated [State] object is not a subtype of
T
.
今回は下の方に該当していた。
TextFormFieldではString型しか受け付けないにも関わらず、GlobalKey<FormFieldState<int>>としてしまったためにフォームの現在値が受け取れなくなっていたことが原因
タイマーで使うパッケージの選定
要求
- カウントアップで時間を測定できること(タイマーというよりはストップウォッチみたいな扱い)
- タイマーの時間を取得できること
- pause機能が存在すること
stop_watch_timer
- カウントアップもダウンも行ける
- pause機能もデフォルトで実装されている
- タイマーの時間もhours,minute,secondごとに取得できる
simple_timer
- カウントアップもダウンも行ける
- pause機能もある
- タイマーを起動する前に時間を決めてタイマーを動かすことしかできないため、採用できない
flutter_timer_countdown
- シンプルで良さげかと思ったが、カウントアップでの計測ができないため不採用
- 公式でstopwatch機能がAPIとして公開されているので、それを使って実装するっている手もある
とりあえず、stop_watch_timerを採用することにした。
これでだめなら、APIを使って自前で実装する
なにがなんでもやらなきゃいけない
- todoを選択
- startを押す(タイマー開始)
- endを押す(タイマー停止)
- 完了を押す(タイマーの現在値をelapsedに打ち込んで更新)
option(タイマーが止まっていなければ止める)
やらなきゃいけないこと
-
振り返り機能
- todo
- タグを入力して振り返りの直下にチップを表示させる(バツボタンで削除できる)
- task
- Taskに含まれているTodoがすべて完了したら、Todoを追加するかTaskの振り返りを行うかを選択できるダイアログを出す
- Taskの振り返りはTodoと同様に振り返りの文章とタグを1つだけ付けることができる
- Taskの振り返りが完了したらタイマー画面に戻る
- todo
-
振り返り参照機能
- TaskまたはTodoを新しく追加する際に名前と一緒にタグを設定するようにする
- 設定したタグの振り返りをいくつか表示する
ユーザーストーリーを考える
- Task,Todoの追加
- Todoの実行
- Todoの振り返り
- Taskの振り返り
- 新しいTask,Todoを追加する際にタグに応じた振り返り内容を表示する
Task -> タスクの名前、タグ
Todo -> Todoの名前、見積もり時間[min]、タグ
タグは「新しく作る」と「すでにあるものを使う」の2種類の方法で選択できる
1行だけの入力ができるTextFieldにタグの名前を入力させる
入力させた文字列に部分一致するタグをTagテーブルから読み込んでリスト形式で表示(リアルタイム)
ソフトウェアキーボードのsubmitを押すとその時点で入力された文字列をタグとして設定する
その文字列と完全一致するタグがTagテーブル上になければ、新規追加する
完全一致するものがあれば、新規追加しない
タグって具体的にどんなのを想定している?
- Task
- 企画書の作成、不具合の修正
- Todo
- ネタ集め、資料作成、原因調査、レビュー依頼
そのTagをTaskやTodoをこなす前につけて、こなした後に振り返りをやるんだよな
こなした後でやっぱりこのTodoに付けるTagはこれじゃないと感じたらどうする?
振り返りを行うタイミングで変更できるようにするか?
TagをTaskやTodoの内容を抽象化したものの名前をつければ問題無しか?
それならTagよりもTypeのほうが良くね?
Tagは振り返りベースな考え、TypeはTask、Todoベースな考え方
TaskやTodoの「タグ」と聞くと、情報を追加したり補足したりするイメージ
TaskやTodoの「タイプ」と聞くと、抽象化したもののイメージ
タグやタイプを振り返りに付ける理由は、同じようなTaskやTodoをやる際に、過去にやったTaskやTodoの振り返りを活用してほしいからである。
タグやタイプがなくても、全文検索で引っ張ってくればよくね?という考えには、前提として振り返りの内容を断片でも覚えておく必要があるために、すっかり振り返りの内容が抜け落ちていればどうしようもない
タグの存在意義は、振り返りの要約・ヘッダー
タイプの存在意義は、振り返りがどういった種類のTask、Todoから生まれたのかを提示する
同じような種類のTaskやTodo間では、振り返りの内容が共有でき、参考になるといった前提で動いている
やること
- TagをTypeに変更する
- initializeQueryの変更
- TaskType(タスクの種類)、TodoType(Todoの種類)テーブルを追加
- それぞれ プライマリキーと種類の名前をカラムとして持つ
- Task、Todoの新規追加時にそれぞれTaskTypeやTodoTypeを設定する
- Typeはdoneになる前までは後からでも変更できるようにする
- Typeは「新しく作る」と「すでにあるものを使う」の2種類の方法で選択できる
- 1行だけの入力ができるTextFieldにタグの名前を入力させる
- 入力させた文字列に部分一致するタグをTaskType, TodoTypeテーブルから読み込んでリスト形式で表示(リアルタイム)
- ソフトウェアキーボードのsubmitを押すとその時点で入力された文字列をタグとして設定する
- その文字列と完全一致するタグがTaskType, TodoTypeテーブル上になければ、新規追加する
- 完全一致するものがあれば、新規追加しない - TaskTypeとTodoTypeで全く同じ名前のTypeがあっても別物判定にする
- 1行だけの入力ができるTextFieldにタグの名前を入力させる
- 振り返りの入力ページでTagを入力させていた部分は消す
- 振り返りの入力だけやる
- Taskに含まれているTodoがすべて完了したら、Todoを追加するかTaskの振り返りを行うかを選択できるダイアログを出す
-
Todoを全部やりきったとしてもやっぱりこれも必要となる場合があるから
- でもそれを許容すると、見積もりもくそもなくならないか?
- このアプリはあくまで見積もり精度向上させるために、PDCAサイクルをぶん回す補助をするアプリだから
- 後から追加したTodoだとわかるようにすればいいのか?
-
Taskに含まれているTodoがすべて完了したら、Taskの振り返りを行うページに遷移させる
見積もり精度向上には
- タスクを実行可能なレベルまでに過不足なく細切れにする
- なぜ?
- あとはやるだけの状態にまで準備しておくことで、タスクをやってる最中にあれもこれも考えずに済み、目の前のタスクに集中できるため
- なぜ?
- 細切れにしたものに対して見積もりを行うことで、大きなタスクに対して見積もりをするよりも精度が向上する
- なぜ?
- 人間は規模が大きいものよりも小さいもののほうが精度よく見積もりができるため
- 仮に見積もりを大きく外したとしても小さいタスクに対して割当た見積もりであるため全体的に見たときの誤差が小さくできる
- なぜ?
-
素朴な疑問としてバッファはどうする?
-
TodoごとではなくTaskに対して最後にバッファを加えるとかするのが定石だがどうする
- 初回リリースは見送る
- TaskとTodoをやるのにバッファは必要不可欠ではあるが、実装の時間が足りるかわからん
少なくとも今日・明日で実装
明後日にストアへ出すための諸々準備
であるため実装に使える時間があまりない
前々からやってればこんなことにはならなかったが、サボりすぎた