Android Room, ViewModel

スポンサーリンク

Background

Room is Android SQL Lite wrapper to use easily.

This example is from Android Room with a View – Kotlin

In this example, use Room / ViewModel / RecyclerView, Coroutine

Different from original code is

  • Use only one activity to understand easily
  • Update library version

This sample explains following technology in Android

  • LiveData
  • ViewModel
  • Entity
  • Room database
  • SQLite
  • DAO (Data Access Object)

Steps

Codes are little bit complicated, so will explain steps

This is Room parts

  1. Import dependencies
  2. Create Model(Entity)
  3. Create DAO (SQL)
  4. Observe database update (Flow)
  5. Add Roomdatabase(Extend class, Singleton with context, work with kapt)
  6. Create Repository with DAO

Next is App

  1. Create ViewModel with Repository
  2. Create Database instance in App layer(Application class) with lazy load
  3. Repository with lazy load
  4. Data access from ViewModel
  5. Activity has viewmodel with repository

Database – Sample Code

build.gradle

Apply kotlin kapt, add dependencies, room, viewmodel, livedata, lifecycle, activity, lifecycle with kapt

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.daiji110.rommapp"
        minSdk 26
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
        freeCompilerArgs += [
                '-Xjvm-default=enable'
        ]
    }
}

dependencies {
    def room_version = "2.4.2"
    def lifecycle_version = "2.5.0-alpha04"
    def coroutines_version = "1.6.0"
    def activity_version = "1.4.0"

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'

    // room
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"

    // lifecycle
    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    // ViewModel utilities for Compose
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    // Lifecycles only (without ViewModel or LiveData)
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

    // Activity
    implementation "androidx.activity:activity-ktx:$activity_version"

    // Saved state module for ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

    // Annotation processor
    kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"

    // coroutines
    api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    // room test
    testImplementation "androidx.room:room-testing:$room_version"
}

Item.kt

Model class.

Add @Entity with tableName

This is data class with value

@Entity(tableName = "item_table")
data class Item(@PrimaryKey val id: Int, val name: String)

ItemDAO.kt

This is Data Access Object (SQL) codes.

@Dao
interface ItemDAO {

    @Query("SELECT * FROM item_table ORDER BY id ASC")
    fun geteAllItems(): Flow<List<Item>>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(item: Item)

    @Query("DELETE FROM item_table")
    suspend fun deleteAll()
}

ItemRoomDatabase

This is abstract class (handle by kapt) extends RoomDatabase class

This manages version, database access , initalization etc…

@Database(entities = [Item::class], version = 1)
abstract class ItemRoomDatabase : RoomDatabase() {

    abstract fun itemDao() : ItemDAO

    companion object {
        @Volatile
        private var INSTANCE: ItemRoomDatabase? = null

        fun getDatabase(
            context: Context,
            scope: CoroutineScope
        ): ItemRoomDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    ItemRoomDatabase::class.java,
                    "item_database"
                )
                    .fallbackToDestructiveMigration()
                    .addCallback(ItemDatabaseCallback(scope))
                    .build()
                INSTANCE = instance
                instance
            }
        }

        private class ItemDatabaseCallback(
            private val scope: CoroutineScope
        ) : RoomDatabase.Callback() {
            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
                INSTANCE?.let { database ->
                    scope.launch(Dispatchers.IO) {
                        populateDatabase(database.itemDao())
                    }
                }
            }
        }

        suspend fun populateDatabase(itemDao: ItemDAO) {
            itemDao.deleteAll()

            // Populate data when create database
            var item = Item(1, "firstitem")
            itemDao.insert(item)
        }
    }
}

ItemRepository.kt

This is repository codes.

Have DAO instance and manipulate DB with DAO. This is interface layer to use DB or other access.

class ItemRepository(private val itemDao: ItemDAO) {

    val allitems: Flow<List<Item>> = itemDao.geteAllItems()

    @Suppress("RedundantSuspendModifier")
    @WorkerThread
    suspend fun insert(item: Item) {
        itemDao.insert(item)
    }
}

This is an preparation to access DB.

Next is app

Application – Sample Codes

ItemViewModel.kt

ViewModel, in this app, get all data and show them as list, add random data when the user press button. Prepare 2 methods to access. And it provides ViewModel instance

class ItemViewModel(private val repository: ItemRepository) : ViewModel() {

    val allItems: LiveData<List<Item>> = repository.allitems.asLiveData()

    fun insert(item: Item) = viewModelScope.launch {
        repository.insert(item)
    }
}

class ItemViewModelFactory(private val repository: ItemRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(ItemViewModel::class.java)) {
            //@SuppressWarnings("UNCHECKED_CAST")
            return ItemViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

RoomApp.kt

This is application class.

Prepare Coroutine scope, Database access ,and repository to access from anywhere in app

class RoomApp : Application() {

    val applicationScope = CoroutineScope(SupervisorJob())

    val database by lazy { ItemRoomDatabase.getDatabase(this, applicationScope) }

    val repository by lazy { ItemRepository(database.itemDao()) }
}

ItemListAdapter.kt

RecyclerView Adapter.

class ItemListAdapter : ListAdapter<Item, ItemListAdapter.ItemViewHolder>(WORDS_COMPARATOR) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        return ItemViewHolder.create(parent)
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val current = getItem(position)
        holder.bind(current.name)
    }

    class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val itemItemView: TextView = itemView.findViewById(R.id.textView)

        fun bind(text: String?) {
            itemItemView.text = text
        }

        companion object {
            fun create(parent: ViewGroup): ItemViewHolder {
                val view: View = LayoutInflater.from(parent.context)
                    .inflate(R.layout.recyclerview_item, parent, false)
                return ItemViewHolder(view)
            }
        }
    }

    companion object {
        private val WORDS_COMPARATOR = object : DiffUtil.ItemCallback<Item>() {
            override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
                return oldItem === newItem
            }

            override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
                return oldItem.name == newItem.name
            }
        }
    }
}

recyclerview_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_orange_light" />
</LinearLayout>

MainActivity.kt

This is main layout codes

class MainActivity : AppCompatActivity() {

    private val charPool : List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')

    private val itemViewModel: ItemViewModel by viewModels {
        ItemViewModelFactory((application as RoomApp).repository)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
        val adapter = ItemListAdapter()
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(this)

        val fab = findViewById<FloatingActionButton>(R.id.fab)
        fab.setOnClickListener {
            var id = 0
            itemViewModel.allItems.let { item ->
                item.value?.let { list ->
                    id = list[list.size-1].id + 1
                }
            }
            val item = Item(id, createItem(6))
            itemViewModel.insert(item)
        }

        itemViewModel.allItems.observe(this) { items ->
            items.let {
                adapter.submitList(it)
            }
        }
    }

    private fun createItem(num: Int) : String {
        if (num < 1) return ""
        val size = charPool.size-1
        var sb = StringBuilder()
        for (i in 1..num) {
            sb.append(charPool[(0..size).random()])
        }
        return sb.toString()
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@drawable/ic_add_black_24dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Completed. Start and can see list items we inserted. We can see + button at right bottom, and press to create new item (random name) and update list realtime

Android
スポンサーリンク
Professional Programmer2

コメント