1
2

More than 1 year has passed since last update.

Roomの実装方法まとめ

Last updated at Posted at 2021-10-12

前々回前回に続き、今回はRoomの実装方法をまとめてみようと思います。

この記事の内容

公式のドキュメントやトレーニング「Android Kotlin の基礎」のレッスン6「Room データベースとコルーチン」を参考に実装のポイントとエッセンスをまとめていきます。

前提知識

  • kotlinの基礎的な文法
  • AndroidStudioの使い方/アプリの作り方
  • 画面や画面部品の配置方法
  • Navigationの実装方法(前々回記事参照)
  • ViewModelの実装方法(前回記事参照)

開発環境

  • Windows 10 Home
  • Android Studio 4.2.1

作成するサンプル

簡単な単語帳アプリを作成します。
Createボタンをクリックすると画面上のListViewに単語の一覧が表示され、Saveボタンのクリックにより内容をローカルDBに保存します。
ローカルDBにデータが保存済みの場合はLoadボタンによりデータを読み込む機能を実装します。
DeleteボタンがクリックされたらローカルDBのデータを消去します。
01.gif
この操作イメージだと少し分かりにくいかもしれませんが、LoadボタンをクリックしたときRoomDBからデータを取得し、その際オーダーを掛けているので画面上のデータの並びも変わっています。

前提となるコード

前述の通りこの記事ではNavigationViewModelについては既知のものとし、詳細は扱いません(詳しくは前回までの記事に書いているので、良かったら見てみてね)。
ただちょっとハンズオンっぽく進めていきたいので、前提となるコードを以下に記します。
また今回はListViewViewModelLiveDataDataBindingにより実装していますが、以下の記事を参考にさせていただきました(大変分かりやすく、参考になりました)。
【Android】LiveData+DataBinding+ViewModelでListView作成

コード上、自分の記事で触れていない部分はコメントを残しています。
動作イメージはこんな感じ。
Saveボタン、LoadボタンはRoomの領域なのでこの時点では未実装です。
またDeleteボタンは画面上の表示を削除するまでの実装です。
02.gif

NavigationViewModelDataBinding用にgradleファイルを設定しておけばとりあえず上記のイメージ通りのアプリにはなります。

レイアウトファイル

activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/myNavHostFragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/navigation"
        app:defaultNavHost="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

fragment_first.xml

アプリ起動時の画面です。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="startViewModel"
            type="com.warpstudio.android.roomsample.StartViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".StartFragment">

        <TextView
            android:id="@+id/startText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Welcome to Room Sample APP!"
            android:textSize="24sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/toSecondFragmentButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="To SecondFragment"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/startText"
            android:onClick="@{() -> startViewModel.onToSecondFragment()}"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

fragment_second.xml

遷移先の画面。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="secondViewModel"
            type="com.warpstudio.android.roomsample.SecondViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".SecondFragment">

            <Button
                android:id="@+id/createButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Create"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/saveButton"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintStart_toStartOf="parent"
                tools:layout_editor_absoluteY="0dp"
                android:onClick="@{()->secondViewModel.creteData()}"/>

            <Button
                android:id="@+id/saveButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Save"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/loadButton"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintStart_toEndOf="@+id/createButton"
                tools:layout_editor_absoluteY="0dp"
                android:onClick="@{()->secondViewModel.saveData()}"/>

            <Button
                android:id="@+id/loadButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Load"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/deleteButton"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintStart_toEndOf="@+id/saveButton"
                tools:layout_editor_absoluteY="0dp"
                android:onClick="@{()->secondViewModel.loadData()}"/>

            <Button
                android:id="@+id/deleteButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Delete"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintStart_toEndOf="@+id/loadButton"
                tools:layout_editor_absoluteY="0dp"
                android:onClick="@{()->secondViewModel.deleteData()}"/>
        </androidx.constraintlayout.widget.ConstraintLayout>

        <ListView
            android:id="@+id/wordList"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </LinearLayout>
</layout>

list_item.xml

ListViewに表示する行単位のレイアウトです。

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="wordData"
            type="com.warpstudio.android.roomsample.WordData" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <TextView
            android:text="@{wordData.word}"
            android:textSize="18sp"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="0.8"/>
        <TextView
            android:text="@{wordData.speech}"
            android:textSize="18sp"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="0.2"/>
        <TextView
            android:text="@{wordData.meaning}"
            android:textSize="18sp"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"/>
    </LinearLayout>
</layout>

navigation.xml

Navigationファイルも一応載せときます。

<navigation 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/navigation"
    xmlns:tools="http://schemas.android.com/tools"
    app:startDestination="@id/startFragment">

    <fragment
        android:id="@+id/startFragment"
        android:name="com.warpstudio.android.roomsample.StartFragment"
        android:label="StartFragment"
        tools:layout="@layout/fragment_start">
        <action
            android:id="@+id/action_startFragment_to_secondFragment"
            app:destination="@id/secondFragment" />
    </fragment>
    <fragment
        android:id="@+id/secondFragment"
        android:name="com.warpstudio.android.roomsample.SecondFragment"
        android:label="SecondFragment"
        tools:layout="@layout/fragment_second"/>
</navigation>

Fragmentクラス

MainActivity.xml は何もいじっていないので省略。

StartActivity.kt

ViewModel使ってますが、次の画面に映るボタンがあるだけなんでわざわざそこまでする必要はないんですけどね……。

class StartFragment : Fragment() {

    private lateinit var binding: FragmentStartBinding
    private lateinit var viewModel: StartViewModel
    private lateinit var viewModelFactory: StartViewModelFactory

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_start,
            container,
            false
        )
        viewModelFactory = StartViewModelFactory()
        viewModel = ViewModelProvider(this, viewModelFactory)
            .get(StartViewModel::class.java)
        viewModel.eventToSecondFragment
            .observe(viewLifecycleOwner, Observer { isToSecond ->
            if (isToSecond) {
                toSecondFragment()
            }
        })
        binding.startViewModel = viewModel

        return binding.root
    }

    private fun toSecondFragment() {
        val action = StartFragmentDirections
            .actionStartFragmentToSecondFragment()
        NavHostFragment.findNavController(this).navigate(action)
        viewModel.onToSecondFragmentComplete()
    }
}

StartViewModel.kt

class StartViewModel : ViewModel() {

    private val _eventToSecondFragment = MutableLiveData<Boolean>()
    val eventToSecondFragment: LiveData<Boolean>
        get() = _eventToSecondFragment

    init {
    }

    fun onToSecondFragment() {
        _eventToSecondFragment.value = true
    }

    fun onToSecondFragmentComplete() {
        _eventToSecondFragment.value = false
    }
}

StartViewModelFactory.kt

class StartViewModelFactory: ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(StartViewModel::class.java)) {
            return StartViewModel() as T
        }
        throw IllegalArgumentException("ERR!")
    }
}

SecondFragment.kt

メインの画面です。
ListViewAdapterを設定しています。

class SecondFragment : Fragment() {

    private lateinit var binding: FragmentSecondBinding
    private lateinit var secondViewModel: SecondViewModel
    private lateinit var secondViewModelFactory: SecondViewModelFactory

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_second,
            container,
            false
        )
        secondViewModelFactory = SecondViewModelFactory()
        secondViewModel = ViewModelProvider(this, secondViewModelFactory)
            .get(SecondViewModel::class.java)

        // wordListのObserverを登録
        secondViewModel.wordList.observe(viewLifecycleOwner, Observer { words ->
            // 作成されたWordListをもとにListViewのデータを更新
            binding.wordList.adapter = WordAdapter(words)
        })

        binding.secondViewModel = secondViewModel
        binding.lifecycleOwner = viewLifecycleOwner

        // ListViewのAdapterを初期化
        binding.wordList.adapter = WordAdapter(ArrayList(0))

        return binding.root
    }
}

SecondViewModel.kt

主役(?)となるViewModelクラス。

class SecondViewModel: ViewModel() {

    private val _wordList = MutableLiveData<List<WordData>>()
    val wordList: MutableLiveData<List<WordData>>
        get() = _wordList

    init {
    }

    // Createボタンクリック時のイベント
    fun creteData() {
        wordList.value = createWordList()
    }

    // Saveボタンクリック時のイベント
    fun saveData() {
    }

    // Loadボタンクリック時のイベント
    fun loadData() {
    }

    // Deleteボタンクリック時のイベント
    fun deleteData(){
        wordList.value = ArrayList(0)
    }

    private fun createWordList() : ArrayList<WordData> {
        var data = arrayListOf(
            WordData("ethos", "名", "気風"),
            WordData("mediocre", "形", "平凡な"),
            // (略)
            WordData("lurch", "動", "よろめく")
        )
        return data
    }
}

SecondViewModelFactory.kt

コピペでOKなViewModelFactoryクラス。

class SecondViewModelFactory: ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(SecondViewModel::class.java)) {
            return SecondViewModel() as T
        }
        throw IllegalArgumentException("ERR!")
    }
}

データ/Adapterクラス

カスタマイズしたListViewDataBindingでデータを表示するために実装するクラスです。

WordData.kt

単語の単語名、品詞、意味を保持するデータクラス。

data class WordData (
    var word: String,
    var speech: String,
    var meaning: String
    )

WordAdapter.kt

ListView用のAdapterクラスです。

// Adapterクラス
class WordAdapter(private var wordDatas: List<WordData>): BaseAdapter() {

    // WordAdapterクラス生成時に呼び出されるメソッド
    override fun getView(
        posistion: Int,
        convertView: View?,
        parent: ViewGroup
    ): View {
        val binding = if (convertView == null) {
            ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        } else {
            DataBindingUtil.getBinding(convertView) ?: throw IllegalStateException()
        }
        with(binding) {
            wordData = wordDatas[posistion]
            executePendingBindings()
        }

        return binding.root
    }

    // その他Overrideが必要なメソッド
    override fun getItem(position: Int) = wordDatas[position]
    override fun getItemId(position: Int) = position.toLong()
    override fun getCount() = wordDatas.size
}

Gradleファイルの更新

作業に入る前にgradleファイルを更新していきます。
Roomはこの辺りに起因するビルドエラーが出やすいので注意してください。

build.gradel(app)ファイルの更新

Roomをか使う場合はkaptプラグインとdependenciesブロックに下記の内容を追加してください。

plugins {
    // (略)
    id 'kotlin-kapt'
}

dependencies {
    // (略)
    // Room
    implementation "androidx.room:room-runtime:2.3.0"
    kapt "androidx.room:room-compiler:2.3.0"
    implementation "androidx.room:room-ktx:2.3.0"
    testImplementation "androidx.room:room-testing:2.3.0"
}

DBの実装

それではDBを実装していきます。
RoomにはEntityDAODatabaseの3要素が必要なので、まずはそれらのクラスを実装していきます。

Entityクラスの作成

新規 Kotlin Class を作成し、data classを定義します。
EntityクラスはDBのテーブルに該当し、各の名前と型、主キーの情報を保持します。
あわせてクラスには@Entityアノテーションとテーブル名の設定が必要です。

@Entity(tableName = "word_data_table")
data class WordEntity (

    @PrimaryKey(autoGenerate = true)
    var wordId: Long = 0L,

    @ColumnInfo(name = "word")
    var word: String,

    @ColumnInfo(name = "speech")
    var speech: String,

    @ColumnInfo(name = "meaning")
    var meaning: String
)

上記では主キーのautoGenerate属性をtrueとしていますが、これによりCREATE等の際に主キーは自動生成されます。

DAOクラスの作成

DAO用に新規 Kotlin Class を作成します。
RoomにおけるDAOはDBに対するCREATEINSERTDELETE等の操作を定義するクラスといったところでしょうか。
DAOinterfaceである必要があります。
また、DBにはコルーチンを使用してアクセスするため、非同期で実行するメソッドに対してsuspendキーワードを設定しておきます。

@Dao
interface WordDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(word: List<WordEntity>)

    @Delete
    suspend fun delete(word: List<WordEntity>)

    @Query("SELECT * FROM word_data_table ORDER BY wordId DESC")
    suspend fun getAllWord(): List<WordEntity>
}

基本的には@Queryアノテーションを設定したメソッドに自分でSQL文を書く必要がありますが、INSERTUPDATE等予め定義された操作に関してはプリセットのアノテーションを利用することもできます。

Databaseクラスの作成

最後にDatabaseクラス(DBホルダークラスト言った方が正しいかな?)を作成します。
DBインスタンスがnullの場合にインスタンスを生成するgetInstanceメソッドを作成します。

@Database(entities = [WordEntity::class], version = 1, exportSchema = false)
abstract class WordDatabase: RoomDatabase() {

    abstract val wordDao: WordDao

    companion object {

        @Volatile
        private var INSTANCE: WordDatabase? = null

        fun getInstance(context: Context): WordDatabase {
            synchronized(this) {
                var instance = INSTANCE

                if (instance == null) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordDatabase::class.java,
                        "database"
                    )
                        .fallbackToDestructiveMigration()
                        .build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }
}

DBへのアクセス

前々章で実装したDBにViewModelからアクセスします。

ViewModel、ViewModelFactoryの改修

ViewModelViewModelFactoryについて、引数にDAOApplicationを受け取るよう改修します。
下記のコードではAndroidViewModelを継承元としています。これはViewModelと基本的には同じですが、コンストラクタのパラメーターとしてアプリケーションのContextを受け取ることができます。

ViewModel

class SecondViewModel(
    val database: WordDao,
    application: Application): AndroidViewModel(application) {
    // 略
}

ViewModelFactory

class SecondViewModelFactory(
    private val dataSource: WordDao,
    private val application: Application): ViewModelProvider.Factory {
    @Suppress("unchecked_cast")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(SecondViewModel::class.java)) {
            return SecondViewModel(dataSource, application) as T
        }
        throw IllegalArgumentException("ERR!")
    }
}

Fragment

UI Controllerの(今回はFragmentonCreate()メソッドでViewModelを生成します。
前述の通りViewModelのコンストラクタにはDAOのインスタンスとアプリケーションのContextが必要なため、個別に作成しておきます。

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // (略)
        // アプリケーションの生成
        val application = requireNotNull(this.activity).application
        // DAOインスタンスの生成
        val dataSource = WordDatabase.getInstance(application).wordDao
        // ViewModelFactoryの生成
        secondViewModelFactory = SecondViewModelFactory(dataSource, application)
        // ViewModelFactoryの生成
        secondViewModel = ViewModelProvider(this, secondViewModelFactory)
            .get(SecondViewModel::class.java)
        // (略)
    }

各処理の実装

ViewModelにDBにアクセスする処理を追記していきます。
DBへのアクセスはすべてコルーチンスコープ内で処理する点に注意してください。

ViewModel

class SleepTrackerViewModel(
        val database: SleepDatabaseDao,
        application: Application) : AndroidViewModel(application) {
    // (略)
    // Createボタンクリック時のイベント
    fun creteData() {
        _wordList.value = createWordList()
    }

    // Saveボタンクリック時のイベント
    fun saveData() {
        // Createが実行され、LiveDataに値が設定されている場合のみ処理を実行
        if (!_wordList.value.isNullOrEmpty()) {
            // DataをEntityのListに置き換え
            val words = _wordList.value!!.map {
                WordEntity(word = it.word, speech = it.speech, meaning = it.meaning)
            } as ArrayList<WordEntity>

            // 保存処理はコルーチンのスコープ内で実行
            viewModelScope.launch {
                database.insert(words)
            }
        }
    }

    // Loadボタンクリック時のイベント
    fun loadData() {
        // コルーチンのスコープ内で実行
        viewModelScope.launch {
            // DBから全レコードを取得
            val allWords = database.getAllWord()

            // EntityをDataのListに置き換え
            val wordList = allWords.map {
                WordData(word = it.word, speech = it.speech, meaning = it.meaning)
            } as ArrayList<WordData>

            // LiveDataにListを設定
            _wordList.value = wordList
        }
    }

    // Deleteボタンクリック時のイベント
    fun deleteData(){
        // LiveDataの削除処理
        if (!_wordList.value.isNullOrEmpty()) {
            _wordList.value = ArrayList(0)
        }

        // DBの処理
        // コルーチンのスコープ内で処理
        viewModelScope.launch {
            val allWords = database.getAllWord()
            database.delete(allWords)
        }
    }
    // (略)
}

Recap

今回は以上です。
Roomはそれ自体は非常に便利なのですが細かなビルドエラーが出やすく、それがちょっと厄介ですね。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2