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:
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.
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.
The created ViewModel can be accessed in Activity like below:
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) } }
Comments
Post a Comment