Roomを使って格納するデータとViewModel連携のあれこれ。
つくりたいもの
- Roomでデータ格納
- 格納するデータをMVVMで表示
- 一部データは後で書き換えられるようにする
手探りで進めた結果ですので参考程度にお願いします。割と雑に書いているけど許して。
つくる
格納するデータ(Entity)定義
こんな感じ。
@Entity(tableName = "prize_history") data class PrizeHistory( @PrimaryKey @ColumnInfo(name = "tweet_id") val tweetId: String, @ColumnInfo(name = "account_id") val accountId: String, @ColumnInfo(name = "tweet_content") val tweetContent: String, @ColumnInfo(name = "closing_date") val closingDate: MutableLiveData<Date> )
この時点で書き換えたいデータはMutableLiveDataにしておく。DAO, Databaseはあまり凝ったことはしていないので調べてください。
Date型値はそのままDBに突っ込めないのでConverterも作成しておく。
class Converters { @TypeConverter fun fromTimestamp(value: Long?): MutableLiveData<Date>? { return value?.let { MutableLiveData(Date(it)) } } @TypeConverter fun dateToTimestamp(date: MutableLiveData<Date>?): Long? { return date?.value?.time } }
表示するレイアウトのxml
RecylerViewで表示する中の、各行のデータレイアウトの一部を抜粋。
<layout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="item" type="com.rutilicus.twitter_prize_management.PrizeHistory" /> <variable name="viewModel" type="com.rutilicus.twitter_prize_management.PrizeHistoryViewModel" /> </data> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/linearlayout_history_item" android:background="@drawable/border"> <!-- 中略 --> <TextView android:id="@+id/textview_content_closing" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight=".20" android:maxLines="1" app:convertDate="@{item.closingDate}" android:onClick="@{() -> viewModel.onClickClosingDate(item)}" /> <!-- 後略 --> </LinearLayout> </layout>
あまり凝ったことはしていないと思うが、Date型の値を表示するためにBindingAdapterを使用している。こんな感じのコード。
object BindingAdapters { @BindingAdapter("convertDate") @JvmStatic fun convertDate(view: TextView, date: MutableLiveData<Date>?) { view.text = date?.value?.let { SimpleDateFormat("MM/dd", Locale.JAPANESE).format(it) } }
ViewModel
インターネット上の情報をもういろいろ見て作りました……
class PrizeHistoryViewModel(application: Application) : AndroidViewModel(application) { private var itemsRaw = mutableListOf<PrizeHistory>() private val _items = MutableLiveData<List<PrizeHistory>>(emptyList()) val items: LiveData<List<PrizeHistory>> = _items.distinctUntilChanged() private val _uiState = MutableStateFlow(PrizeHistoryUiState()) val uiState: StateFlow<PrizeHistoryUiState> = _uiState private var dao: PrizeHistoryDao? init { if (DatabaseInstance.database == null) { DatabaseInstance.createDatabase(getApplication<Application>().applicationContext) } dao = DatabaseInstance.database?.prizeHistoryDao() } fun loadPrizeHistory() { val loadItems = mutableListOf<PrizeHistory>() CoroutineScope(Dispatchers.Main).launch { withContext(Dispatchers.Default) { // DBからデータ読み出し・設定 dao?.getAll()?.forEach { loadItems.add(it) } } itemsRaw = loadItems _items.value = ArrayList(itemsRaw) } } fun updateClosingDate(item: PrizeHistory, newDate: Date) { itemsRaw.forEach { if (it.tweetId == item.tweetId) { it.closingDate.value = newDate CoroutineScope(Dispatchers.Default).launch { dao?.updateAll(it) } return@forEach } } } fun onClickClosingDate(item: PrizeHistory) { viewModelScope.launch { _uiState.update { it.copy(dateFixItem = item) } return@launch } } fun dateFixDialogShown() { _uiState.update { it.copy(dateFixItem = null) } } }
まず、DBアクセスのビジネスロジックをViewModelに入れる都合上、Contextが必要になるのでAndroidViewModelを継承する。initでアクセス用の初期処理を実行。
loadPrizeHistoryは初期表示時のデータ一斉読み込みメソッド。Mainスコープであれこれやるよう書いているけど不要かも。
updateClosingDateは格納するデータのうち後から書き換え対象となるDate型オブジェクトを更新するメソッド。MutableLiveDataの書き換えとDBの書き換えを同じメソッド内で行う。DBアクセスはMainスコープでできないのでDefaultスコープで実行。
onClickClosingDateは更新データ入力用のダイアログを開くトリガを発火させるメソッド。dateFixDialogShownはダイアログ表示後の後処理メソッド。
ダイアログはUI要素なのでViewModel中には記載しないようにする。StateをUI側で監視して、状態に変化があればダイアログを開くようにする。
Stateを格納するdata classはなんてことのないクラス。
data class PrizeHistoryUiState( val dateFixItem: PrizeHistory? = null, )
Fragment
表示するUI側の実装。
class HistoryFragment : Fragment() { private lateinit var prizeHistoryListAdapter: PrizeHistoryListAdapter private lateinit var viewModel: PrizeHistoryViewModel override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { activity?.application?.let { viewModel = ViewModelProvider( it as MyApplication, PrizeHistoryViewModelFactory(it))[PrizeHistoryViewModel::class.java] } return FragmentHistoryBinding.inflate(inflater, container, false) .apply { recyclerviewHistory.run { layoutManager = LinearLayoutManager(context) adapter = PrizeHistoryListAdapter(viewLifecycleOwner, this@HistoryFragment.viewModel).also { prizeHistoryListAdapter = it } } }.run { root } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // データ読み出し処理 viewModel.loadPrizeHistory() viewModel.run { items.observe(viewLifecycleOwner) { prizeHistoryListAdapter.submitList(it) } } viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { uiState -> uiState.dateFixItem?.let { Calendar.getInstance().apply { val item = uiState.dateFixItem this.time = item.closingDate.value!! this@HistoryFragment.context?.let { context -> DatePickerDialog( context, { _, year, month, dayOfMonth -> Calendar.getInstance().apply { this.set(year, month, dayOfMonth) viewModel.updateClosingDate(item, this.time) } }, this.get(Calendar.YEAR), this.get(Calendar.MONTH), this.get(Calendar.DAY_OF_MONTH), ).show() } } viewModel.dateFixDialogShown() } } } } } }
onCreateViewの処理については定型のViewModelを使用する場合の処理とは離れていますが、これは別Activityからデータの追加処理を行う都合上こうなっています。Applicationを継承したクラスでViewModelStoreを保持するようにしています。それ以外は凝ったことはしていないはず。
onViewCreatedではデータの初期化処理と、データ更新時のRecyclerViewへの反映のための監視、ダイアログを開くためのトリガ発火時の処理を記載。一応動いているつもりではあるが、あまり自信はない……
できたアプリ
こんな感じのアプリになりました。なんだかクラッシュしているようなのだが、Android Vitalsからレポートが見れない(報告されていない?)ので何が起こっているのか分からない……
play.google.com
開発用端末で動作するのに常用端末で動き見ていたらクラッシュすることあるのでAndroidはわけがわからない
— rutilicus (@rutilicus927) 2022年11月4日