Mokelab Blog

#62 AndroidでToDoアプリを作る - ToDoの削除機能

今回はToDoの削除機能を実装してみます。

削除の手順

削除は気軽にできないほうが安全なので、次の手順で削除が実行されるようにしてみます。

順に実装していきましょう。

メニューに削除の項目を追加する

まず、メニューに項目を追加しましょう。今回はアイコン表示しないので、vector assetsの追加は行いません。

詳細画面のメニューに追加するので、menu_detail.xmlを開きます。そして次のようにMenu Itemを追加します。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:android="http://schemas.android.com/apk/res/android">
  <item
    android:id="@+id/action_edit"
    android:icon="@drawable/ic_baseline_edit_24"
    android:title="Edit"
    app:showAsAction="ifRoom" />
  <item
    android:id="@+id/action_delete"
    android:title="@string/delete"
    app:showAsAction="never" />
</menu>

IDはaction_deleteとしておきます。アイコン(アクション)として表示させたくないので、app:showAsActionにはneverを指定します。

確認用のダイアログを追加する

次に削除確認用のダイアログを追加します。ダイアログはDialogFragmentを使って作ります。今回はcom.mokelab.mytodo.dialogパッケージを作り、その中にConfirmDialogFragmentというクラスを追加してみます。

package com.mokelab.mytodo.dialog

import androidx.fragment.app.DialogFragment
  
class ConfirmDialogFragment: DialogFragment() {
}

DialogFragmentでは、onCreateDialog()をオーバーライドし、その中でAlertDialogを作ります。importを追加する際、AndroidX版を追加しましょう。

class ConfirmDialogFragment: DialogFragment() {
  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    return AlertDialog.Builder(requireActivity()).apply {
      setMessage("このToDoを削除しますか?")
      setPositiveButton(android.R.string.ok, listener)
      setNegativeButton(android.R.string.cancel, listener)
    }.create()
  }
  
  private val listener = DialogInterface.OnClickListener { _, which -> 
      // 実装は次節
  }
}

setPositiveButton()でOKボタンを追加し、setNegativeButton()でキャンセルボタンを追加します。ボタンタップ時の処理はlistenerの中で記述します。前回の「呼び出し元フラグメントに結果を伝える」と同様に結果を呼び出し元に伝えます。

class ConfirmDialogFragment: DialogFragment() {
  ...

  private val listener = DialogInterface.OnClickListener { _, which ->
    val data = bundleOf("result" to which)
    parentFragmentManager.setFragmentResult("confirm", data)
  }
}

ナビゲーショングラフに追加する

ダイアログもナビゲーショングラフの遷移先にすることができます。ConfirmDialogFragmentをnav_main.xmlに追加します。

追加したら、詳細画面からの遷移を追加します。

メニュータップ時の処理を記述する

「削除」をタップした際、ダイアログが表示されるようにします。ToDoDetailFragmentonOptionsItemSelected()を修正します。

@AndroidEntryPoint
class ToDoDetailFragment: Fragment(R.layout.todo_detail_fragment) {
  ...
  override fun onOptionsItemSelected(item: MenuItem): Boolean {
    return when (item.itemId) {
      R.id.action_edit -> {
        ...
      }
      // このブロックを追加
      R.id.action_delete -> {
        findNavController()
            .navigate(R.id.action_toDoDetailFragment_to_confirmDialogFragment)
        true
      }      
      else -> super.onOptionsItemSelected(item)
    }
  }
}

ダイアログの選択結果を扱う

確認ダイアログでボタンがタップされた後の処理をToDoDetailFragmentに記述します。扱い方は前回と同様にsetFragmentResultListener()を使います。OKがタップされていたらViewModelに削除処理を依頼します(実装は次節)。

class ToDoDetailFragment: Fragment(R.layout.todo_detail_fragment) {
  private val vm: ToDoDetailViewModel by viewModels()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    setFragmentResultListener("confirm") { _, data ->
      val which = data.getInt("result", DialogInterface.BUTTON_NEGATIVE)
      if (which != DialogInterface.BUTTON_POSITIVE) return@setFragmentResultListener

      vm.delete() // 次節で実装する
    }
  }
}

ToDo削除処理の実装

実際のToDo削除処理はこれまでと同様にViewModelで実装します。ToDoDetailViewModelを次のようにします。

@HiltViewModel
class ToDoDetailViewModel @Inject constructor(
  private val toDoRepository: ToDoRepository,
  savedStateHandle: SavedStateHandle
): ViewModel() {
  val todo = savedStateHandle.getLiveData<ToDo>("todo")
  val errorMessage = MutableLiveData<String>()
  val deleted = MutableLiveData<Boolean>()

  fun delete() {
    viewModelScope.launch {
      try {
        val todo = this@ToDoDetailViewModel.todo.value ?: return@launch
        toDoRepository.delete(todo)
        deleted.value = true
      } catch (e: Exception) {
        errorMessage.value = e.message
      }
    }
  }
}

Hiltの導入がまだだったので、クラスに対し@HiltViewModelをつけ、コンストラクタに@Inject constructorをつけます。また、削除処理で使うのでToDoRepositoryをプロパティとしてもたせます。

削除メソッドの中ではこれまでと同様にリポジトリのメソッドを呼ぶだけの実装にします。削除対象のToDoはtodoプロパティの中に入っているので、それを使います。

ToDoRepositoryにはまだdelete()メソッドがないので、Alt+Enterで追加していきます。インターフェースは次のようにsuspend funにしておきます。

interface ToDoRepository {
  ...
  suspend fun delete(value: ToDo)
}

実装クラスとなるToDoRepositoryImplでは、DAOのメソッドを呼ぶだけにします。

class ToDoRepositoryImpl @Inject constructor(
  private val dao: ToDoDAO
): ToDoRepository {
  ...
  override suspend fun delete(todo: ToDo) {
    dao.delete(todo)
  }
}

削除処理完了後の処理の記述

リポジトリにToDoを削除させた後の処理を記述します。ToDoDetailFragmentLiveDataを監視します。

@AndroidEntryPoint
class ToDoDetailFragment: Fragment(R.layout.todo_detail_fragment) {
  private val vm: ToDoDetailViewModel by viewModels()
  ...
  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)  
    this._binding = TodoDetailFragmentBinding.bind(view)
    ...
    vm.errorMessage.observe(viewLifecycleOwner) { msg ->
      if (msg.isEmpty()) return@observe
  
      Snackbar.make(requireView(), msg, Snackbar.LENGTH_SHORT).show()
      vm.errorMessage.value = ""
    }
    vm.deleted.observe(viewLifecycleOwner) {
      // メイン画面まで戻る。戻り先を指定しないとタイミングによっては
      // ダイアログを閉じる操作になってしまう
      findNavController().popBackStack(R.id.mainFragment, false)
    }
  }
  ...
}

削除の完了を表すdeletedでは、引数ありpopBackStack()を使います。引数なしの場合、処理のタイミングによってはダイアログを閉じる操作となり、メイン画面に戻らないことがあるためです。

動作確認してみる

ここまでできたら動作確認をしてみましょう。

当然のような動きですが、削除を選び、確認ダイアログでOKを押すと削除されました。

まとめ

今回はToDoの削除機能を実装してみました。ダイアログの作り方や扱い方はこのシリーズで初めて登場したので、今後のアプリ開発でも使えるようになっておきましょう。

本サイトではサービス向上のため、Google Analyticsを導入しています。分析にはCookieを利用しています。