#56 AndroidでToDoアプリを作る - ToDo作成画面を作る
今回はToDo作成画面を作り、データベースにToDoを保存できるようにします。
画面レイアウトを作る
ToDoのタイトルと詳細を入力するためのEditTextを配置します。タイトルは1行テキストで、詳細は複数行を入力できるようにします。

EditTextはそのまま配置せず、TextInputLayoutを配置し、その中にいれるようにします。TextInputLayoutは、ヒント文字列をフローティングさせたり、文字数カウンタなどを表示させたりするためのレイアウトです。PaletteでTextInputLayoutを検索し、レイアウトに貼り付けるとよいでしょう。

また、以下の設定を行います。
- 上下左右のマージンを16dpにする
- タイトル用EditTextのIDを
@+id/title_edit
にする - タイトル用EditTextのinputTypeを
text
- 詳細用EditTextのIDを
@+id/detail_edit
にする - 詳細用EditTextのinputTypeを
textMultiLine
にする - 詳細用EditTextのgravityを
top|start
にする - 詳細用EditTextの高さを
match_parent
にする -
詳細用EditTextのminLinesを
2
にする(ヒントラベルを左上に配置するために必要)
レイアウトXMLは次のようになります。とても長いのでみたい方のみ表示ボタンを押してください。
マテリアルデザインライブラリ
TextInputLayoutを配置すると、プロジェクト作成時の状況によっては次のように下線のみの入力欄が表示される場合があります。

これは、アプリで使用しているテーマがTheme.MaterialComponents
ではなく、Theme.AppCompat
だったりするのが原因です。
マテリアルデザインのテーマを使用することで、他アプリにあわせたユーザー体験が提供できます。app/build.gradleにライブラリを追加しましょう。
dependencies {
...
implementation 'com.google.android.material:material:1.2.1'
}
ライブラリを追加したら、右上のSync nowをクリックするのを忘れないようにします。
そして、res/values/styles.xml(or themes.xml)のAppThemeの親を変更します。
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
</style>
</resources>
最後にAndroid Studioの上メニューの「Build」→「Make Project」を選び、一度ビルドします。この作業をしないとレイアウトエディタでの表示が更新されません。
メニューを作る
次に、ToDo保存ボタンとなるメニューを作ります。resフォルダにmenu
フォルダを作り、その中に「File」→「New」→「Menu
Resource
File」でメニュー用XMLファイルを作ります。ファイル名はmenu_create
にしておきます。

保存を表すアイコンをアプリに追加していないので、Vector assetで追加します。保存を表すsaveアイコンも提供されていますが、スマホアプリでほとんど見かけません(知っていたらTwitterで教えてください)。完了を表す「done」アイコンを追加します。

ここからmenu_create.xmlの中身の編集です。メニューの項目を追加するには、レイアウトエディタと同様に左上のPaletteからMenuItemを選び、中央の画面にドラッグします。

追加したメニューの項目に対し、次の設定をします。
- IDを
@+id/action_save
にする - Titleをsaveにする
-
iconを先ほどアプリに追加した
@drawable/ic_baseline_done_24
にする - showAsActionを
ifRoom
にする

XMLは次のようになります。
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_baseline_done_24"
android:title="@string/save"
app:showAsAction="ifRoom" />
</menu>
このmenu_create.xmlをToDo作成画面で使うための処理を記述していきます。まず、onCreate()
をオーバーライドし、setHasOptionsMenu(true)
を呼びます。
@AndroidEntryPoint
class CreateToDoFragment: Fragment(R.layout.create_todo_fragment) {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
}
次に、onCreateOptionsMenu()
をオーバーライドし、引数で渡されるMenuInflater
を使ってMenu
をXMLリソースから作ります。
@AndroidEntryPoint
class CreateToDoFragment: Fragment(R.layout.create_todo_fragment) {
...
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
// MenuInflaterを使ってXMLリソースからメニューを作る
inflater.inflate(R.menu.menu_create, menu)
}
}
ここまで書いたら実行し、ToDo作成画面を表示してみます。右上にDoneアイコンが表示されていればメニューの設定はできています。

保存処理を記述する
メニューのDoneボタンをタップした際の処理を記述していきます。メニュー項目をタップした際の処理はonOptionsItemSelected()
に記述します。
class CreateToDoFragment: Fragment(R.layout.create_todo_fragment) {
private val vm: CreateToDoViewModel by viewModels()
private var _binding: CreateTodoFragmentBinding? = null
private val binding: CreateTodoFragmentBinding get() = _binding!!
...
override fun onOptionsItemSelected(item: MenuItem) =
when (item.itemId) {
R.id.action_save -> {
save()
true
}
else -> super.onOptionsItemSelected(item)
}
private fun save() {
// 入力内容を取得して
val title = binding.titleEdit.text.toString()
val detail = binding.detailEdit.text.toString()
// ViewModelに渡すのみ
vm.save(title, detail)
}
}
タップされた際、入力内容を取得して保存処理を開始します。
CreateToDoViewModel
にsave()
メソッドはまだ作っていないので、作ります。Alt+Enterを使ってAndroid
Studioに作らせるのがよいでしょう。
class CreateToDoViewModel: ViewModel() {
fun save(title: String, detail: String) {
// タイトルが空っぽだったらエラーメッセージを出す
// リポジトリ経由で実際の保存処理を行う
}
}
エラーメッセージの表示はLiveData
経由でやってみます。MutableLiveData
をプロパティとして用意し、エラーとなった場合に書き込みます。
class CreateToDoViewModel: ViewModel() {
val errorMessage = MutableLiveData<String>()
fun save(title: String, detail: String) {
// タイトルが空っぽだったらエラーメッセージを出す
if (title.trim().isEmpty()) {
errorMessage.value = "Please input title"
return
}
// リポジトリ経由で実際の保存処理を行う
}
}
フラグメント側でerrorMessage
を監視する処理も書きましょう。今回はSnackbarを使ってみます。
class CreateToDoFragment: Fragment(R.layout.create_todo_fragment) {
private val vm: CreateToDoViewModel by viewModels()
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
this._binding = CreateTodoFragmentBinding.bind(view)
vm.errorMessage.observe(viewLifecycleOwner) { msg ->
if (msg.isEmpty()) return@observe
Snackbar.make(requireView(), msg, Snackbar.LENGTH_SHORT).show()
// 画面回転するともう一度エラーが表示されちゃうのを防止
vm.errorMessage.value = ""
}
}
}
ViewModel用Hiltライブラリの追加
エラーチェックの後はリポジトリの回で作成したToDoリポジトリの作成メソッドを呼びます。次のようにコンストラクタでToDoRepository
を受け取るようにします。
class CreateToDoViewModel(
private val repo: ToDoRepository
): ViewModel() { ... }
コンストラクタを追加しアプリを実行すると、ToDo作成画面を表示しようとした時点でクラッシュします。ログには次のようなメッセージが表示されます。
java.lang.RuntimeException:
Cannot create an instance of class com.mokelab.mytodo.page.create.CreateToDoViewModel
ViewModelのオブジェクトはby viewModels()
で作成されるので、引数なしコンストラクタである必要があります(AndroidViewModel
の場合はApplication
のみ受け取るコンストラクタ)。
ここで登場するのがViewModel用Hiltライブラリです。これを使うと
-
ViewModelProvider.Factory
を自作しなくても引数ありコンストラクタなViewModelが扱える - DIで必要となるオブジェクトを差し込んでくれる
といったメリットがあります。公式ドキュメントはこちらです(英語)。
まず、app/build.gradleにライブラリを追加します。
dependencies {
...
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha03'
}
ライブラリを追加したら、右上の「Sync now」をクリックするのを忘れないようにします。
そして、CreateToDoViewModelクラスに@HiltViewModel
をつけ、コンストラクタに@Inject constructor
をつけます。これだけでコンストラクタの引数となっているオブジェクトをHiltが作り、差し込んでくれます。
@HiltViewModel
class CreateToDoViewModel @ViewModelInject constructor(
private val repo: ToDoRepository
): ViewModel() { ... }
なお、by viewModels()
でセットするViewModelに@HiltViewModel
がついている場合、そのフラグメントには@AndroidEntryPoint
をつける必要があります。もしつけ忘れていた場合、単純に引数ありコンストラクタを追加したときと同じRuntimeException
が発生します。
ToDoRepository
をHilt経由でもらうことに成功したので、さっそくToDoを保存させてみます。保存処理は非同期で行われるので、viewModelScope
の中で実行します。こうすることで、保存処理中に画面回転が発生しても処理は継続され、バックボタンなどでViewModelが破棄されるタイミングでキャンセルされるようになります。完了したらLiveDataに「終わったよ」をセットしUI側に伝えることにします。
@HiltViewModel
class CreateToDoViewModel @Inject constructor(
private val repo: ToDoRepository
) : ViewModel() {
val errorMessage = MutableLiveData<String>()
val done = MutableLiveData<Boolean>()
fun save(title: String, detail: String) {
// タイトルが空っぽだったらエラーメッセージを出す
if (title.trim().isEmpty()) {
errorMessage.value = "Please input title"
return
}
// リポジトリ経由で実際の保存処理を行う
viewModelScope.launch {
try {
repo.create(title, detail)
done.value = true
} catch (e: Exception) {
errorMessage.value = e.message
}
}
}
}
done
の中身もフラグメント側で監視し、終わったら1つ前の画面に戻るようにしましょう。1つ前の画面に戻るには、を呼びます。
class CreateToDoFragment : Fragment(R.layout.create_todo_fragment) {
private val vm: CreateToDoViewModel by viewModels()
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
this._binding = CreateTodoFragmentBinding.bind(view)
vm.errorMessage.observe(viewLifecycleOwner) { msg ->
...
}
vm.done.observe(viewLifecycleOwner) { done ->
if (!done) return@observe
// 1つ前の画面に戻る
findNavController().popBackStack()
}
}
}
これで保存処理が記述できました。
実行してみる
タイトルと詳細を入力し、右上のボタンを押すとToDo保存処理が行われ、完了すると1つ前の画面に戻っています。

Database Inspector
ToDoの保存は行われているようですが、現時点でアプリに表示する機能がないため、本当に保存できているかわかりません。そこで登場するのがDatabase Inspectorです。アプリ内のデータベースの内容を確認することができます。Android Studio 4.1で追加された機能で、API 26以降の環境でデバッグ実行した際に使えます。
Database Inspectorを使うには、Android Studioの上メニューの「View」→「Tool Windows」→「Database Inspector」を選びます。すでにウィンドウの一番下にタブとして表示されている場合は、タブ部分をクリックします。

Database Inspectorが正しく動作した場合、左側にアプリ内のデータベースとテーブルが表示されます。

ToDoテーブルをダブルクリックすると、右側にテーブルの内容が表示されます。動作確認で追加したToDoがちゃんと保存されていることが確認できました。

まとめ
今回はToDo作成画面を作り、データベースにToDoを保存できるようにしました。ViewModelを使った処理は他のアプリ開発でも有用なので、マスターしておきましょう。