Room with LiveData, ViewModel - Android Architecture Components - Kotlin


Introduction:


We have already learnt about Room in last post, Room-Kotlin. In this post, lets learn about how to use ViewModel and LiveData with Room to improve usablity. Using LiveData with Room, allows views to be notified about data changes automatically.

LiveData:

LiveData is nothing but observable data holder class. It allows us to observer changes in data across multiple components of app. It is also aware of and respects lifecycle of Activities/Fragments. It also ensures that LiveData only updates app component observers that are in an active lifecycle state.

The following image depicts the Lifecycle and States:

Lifecycle and its states
Here, CREATED, STARTED, RESUMED - Active States
          INTIALIZED, DESTROYED - InActive States

So, LiveData will remove the updates automatically while in InActive States, i.e., INTIALIZED and DESTROYED.

ViewModel:

ViewModel is an entity that is free of Activity/Fragment's lifecycle. So it allows us to store and manage UI-related data in lifecycle conscious way. For example, it can retain its state/data even when the configuration changed. It doesn't contain any code related to UI. 

Implementing ViewModel:

Every ViewModel class should extends ViewModel class. If it needs to use the ApplicationContext, then it should extends AndroidViewModel.

class MyViewModel : ViewModel() {
    var users: MutableLiveData<List<User>>? =null

    fun getUsers(): LiveData<List<User>> {
        if (users == null) {
            users = MutableLiveData<List<User>>()
            loadUsers()
        }
        return users
    }

    private fun loadUsers() {
        // Do an asyncronous operation to fetch users.
    }
}

The created ViewModel can be accessed in Activity like below:


class MyActivity : AppCompatActivity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        val model = ViewModelProviders.of(this).get(MyViewModel::class.java)
        model.getUsers().observe(this, { users ->
            // update UI
        })
    }
}

Code:

build.gradle:

Add the following plugin in app module's build.gradle.
         apply plugin: 'kotlin-kapt'
Also, add the following in dependencies in the same:
         compile 'com.android.support:recyclerview-v7:27.0.2'
         compile 'com.android.support:cardview-v7:27.0.2'
         compile "android.arch.persistence.room:runtime:1.0.0"
         kapt "android.arch.persistence.room:compiler:1.0.0"
         compile "android.arch.lifecycle:extensions:1.0.0"
         kapt "android.arch.lifecycle:compiler:1.0.0"

Book.kt (Entity - Class)


@Entity
data class Book(var bookName : String, var author : String,
                var genre : String){
    @PrimaryKey(autoGenerate = true)
    var id : Long? = null
}

BookDao.kt (Dao - Interface)


@Dao
interface BookDao {
    @Query("SELECT * FROM Book")
    fun getBookInfo() : LiveData<MutableList<Book>>

    @Insert
    fun addBook(book : Book)

    @Update(onConflict = REPLACE)
    fun updateBook( book: Book)

    @Delete
    fun deleteBook( book: Book?)
}

MyDatabase.kt (Database - Class)

Every Database operation calls should be in Background thread. Otherwise, the app will crash.

@Database(entities = arrayOf(Book::class), version = 1)
abstract class MyDatabase : RoomDatabase() {
    abstract fun bookDao(): BookDao

    companion object {
        var INSTANCE: MyDatabase? = null

        fun getInstance(context: Context): MyDatabase {

            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(context.applicationContext, MyDatabase::class.java, "Books.db").build()
            }
            return INSTANCE as MyDatabase;
        }

        @SuppressLint("StaticFieldLeak")
        fun insertData(mydata: MyDatabase, book: Book) {
            object : AsyncTask<Void, Void, Void>() {
                override fun doInBackground(vararg voids: Void): Void? {
                    mydata.bookDao().addBook(book)
                    return null
                }
            }.execute()
        }

        @SuppressLint("StaticFieldLeak")
        fun getData(mydata: MyDatabase): LiveData<MutableList<Book>> {
            lateinit var lists: LiveData<MutableList<Book>>

            return object : AsyncTask<Void, Void, LiveData<MutableList<Book>>>() {
                override fun doInBackground(vararg voids: Void): LiveData<MutableList<Book>>? {
                    lists = mydata.bookDao().getBookInfo()
                    return lists
                }
            }.execute().get()
        }

        @SuppressLint("StaticFieldLeak")
        fun deleteData(mydata: MyDatabase, book: Book?) {
            object : AsyncTask<Void, Void, Void>() {
                override fun doInBackground(vararg voids: Void): Void? {
                    mydata.bookDao().deleteBook(book)
                    return null
                }
            }.execute()
        }
    }
}

BookViewModel.kt (ViewModel - Class)

Here, we are using AndroidViewModel. It will handle the data fetching from Database and it have to be get and return as LiveData for observing.

class BookViewModel(application: Application) : AndroidViewModel(application) {
    var list: LiveData<MutableList<Book>>

    init {
       list =  MyDatabase.getData(MyDatabase.getInstance(this.getApplication()))
    }

    fun fetchAllData() : LiveData<MutableList<Book>> = list
}

RecyclerAdapter.kt:

Adapter class for RecyclerView to display the list of data fetched from Database.

class RecyclerAdapter(val context: Context, var data : MutableList<Book>?) : RecyclerView.Adapter<RecyclerAdapter.Holder>() {

    override fun onBindViewHolder(holder: Holder?, position: Int) {
        holder?.bindItems(data?.get(position))
    }

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder {
        val v = LayoutInflater.from(context).inflate(R.layout.recycler_item, parent, false)
        return Holder(v)
    }

    override fun getItemCount(): Int = data?.size?:0

    class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView){

        fun bindItems(book: Book?){
            itemView.name.text = book?.bookName
            itemView.author.text = book?.author
            itemView.genre.text = book?.genre
            itemView.setOnLongClickListener(object :View.OnLongClickListener{
                override fun onLongClick(v: View?): Boolean {
                    MyDatabase.deleteData(MyDatabase.getInstance(itemView.context), book)
                    return true
                }
            })
        }
    }

    fun addItems(t: MutableList<Book>?) {
        data = t
        notifyDataSetChanged()
    }
}

DbActivity.kt 

ViewModel class should be registered in this Activity. Data should be fetched and observed using LiveData. If the changes detected in Database, it will invoke onChanged(), there the adapter can be notified of those changes.

class DbActivity : AppCompatActivity() {

    lateinit var adapter: RecyclerAdapter
    lateinit var viewModel: BookViewModel
    var list: MutableList<Book>? = null

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

        recyclerView.layoutManager = LinearLayoutManager(this)
        adapter = RecyclerAdapter(this, list)
        recyclerView.adapter = adapter

        viewModel = ViewModelProviders.of(this).get(BookViewModel::class.java)
        viewModel.fetchAllData().observe(this, object : Observer<MutableList<Book>> {
            override fun onChanged(t: MutableList<Book>?) {
                Log.v("OnChanged","OnChanged!!")
                adapter.addItems(t)
            }
        })
        
        add.setOnClickListener {
            openDialog()
        }

    }

    private fun openDialog() {
        val dialog = Dialog(this)
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
        dialog.setContentView(R.layout.dialog)
        val lp: WindowManager.LayoutParams = WindowManager.LayoutParams().apply {
            copyFrom(dialog.window.attributes)
            width = WindowManager.LayoutParams.MATCH_PARENT
            height = WindowManager.LayoutParams.WRAP_CONTENT
        }

        val submit = dialog.findViewById<View>(R.id.submit) as TextView
        val name = dialog.findViewById<View>(R.id.name) as EditText
        val author = dialog.findViewById<View>(R.id.author) as EditText
        val genre = dialog.findViewById<View>(R.id.genre) as EditText

        submit.setOnClickListener {
            when {
                name.length() == 0 || author.length() == 0 || genre.length() == 0 ->
                    Toast.makeText(this@DbActivity, "Please fill all the fields"
                            , Toast.LENGTH_SHORT).show()

                else -> {
                    val book = Book(name.text.toString(), author.text.toString(), genre.text.toString())
                    MyDatabase.insertData(MyDatabase.getInstance(this), book)
                    dialog.dismiss()
                }
            }
        }
        dialog.show()
        dialog.getWindow().setAttributes(lp)
    }
}

Run Application:


Comments

Popular posts from this blog

SOAP Client using ksoap2 in Android - Kotlin

RecyclerView with different number of columns using SpanSizeLookup

Using Camera in Android - Kotlin

Databinding in RecyclerView - Android - Kotlin

Map, Location update and AutoComplete Places - Kotlin

Stripe Integration in Android - Kotlin

Braintree Integration in Android - Kotlin

Android JetPack - Scheduling Tasks with WorkManager

Using RxJava, Retrofit in Android - Kotlin