Each component has its own responsibilities to do their task
- The View receives user action and send it to the ViewModel, or listen live data stream from ViewModel and shows it to user.
- The ViewModel receives user actions from the View or provides data to View.
- The Model abstract the data sourec. We write our business logic here and both View and ViewModel uses that to stream data.
We implement Android lifecycle, Room persistence library and Androidx here.
def materialDesignVersion = '1.0.0'
def constraintlayoutVersion = '1.1.3'
def appcompatVersion = '1.1.0'
def roomDbVersion = '1.1.1'
def lifecycleVersion = '1.1.1'
def espressoVersion = '3.2.0'
def junitVersion = '4.12'
def testRunnerVersion = '1.2.0'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.core:core-ktx:1.1.0'
implementation "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion"
implementation "com.google.android.material:material:$materialDesignVersion"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
//dependency for architecture component
implementation "android.arch.lifecycle:extensions:$lifecycleVersion"
implementation "android.arch.lifecycle:viewmodel:$lifecycleVersion"
//dependency for room persistance database
implementation "android.arch.persistence.room:runtime:$roomDbVersion"
kapt "android.arch.persistence.room:compiler:$roomDbVersion"
//dependency for coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$testRunnerVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
}
I'm not using any dependency injection here right now. With keeping newbies in mind i published the application. I've added some base class here for making a generic adapter and also a base class for Activity.
abstract class BaseActivity : AppCompatActivity (){
abstract fun layoutRes(): Int
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(layoutRes())
}
}
Here are some important classes for making a generic adapter
interface BaseRecyclerListener {
}
abstract class BaseViewHolder <T,L : BaseRecyclerListener>(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun onBind(item: T, listener: L?)
}
And finaly the BaseRecyclerAdapter
abstract class BaseRecyclerAdapter<T, L : BaseRecyclerListener, VH : BaseViewHolder<T, L>>(val context: Context): RecyclerView.Adapter<VH>()
{
internal var items: MutableList<T>? = null
private var listener: L? = null
private lateinit var layoutInflater: LayoutInflater
private var position: Int = 0
init {
layoutInflater = LayoutInflater.from(context)
items = ArrayList<T>()
}
abstract override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): VH
override fun onBindViewHolder(p0: VH, @SuppressLint("RecyclerView") p1: Int) {
val item = items!!.get(p1)
p0.onBind(item, listener)
position = p1
}
override fun getItemCount(): Int {
return if (items != null) items!!.size else 0
}
fun setItems(items: List<T>?) {
if (items == null) {
throw IllegalArgumentException("Cannot set `null` item to the Recycler adapter")
}
this.items!!.clear()
this.items!!.addAll(items)
notifyDataSetChanged()
}
fun getItems(): MutableList<T>? {
return this.items
}
fun add(item: T?) {
if (item == null) {
throw IllegalArgumentException("Cannot add null item to the Recycler adapter")
}
items!!.add(item)
notifyItemInserted(items!!.size - 1)
}
fun addAll(items: List<T>?) {
if (items == null) {
throw IllegalArgumentException("Cannot add `null` items to the Recycler adapter")
}
this.items!!.addAll(items)
notifyItemRangeInserted(this.items!!.size - items.size, items.size)
}
fun clear() {
items!!.clear()
notifyDataSetChanged()
}
fun remove(item: T) {
val position = items!!.indexOf(item)
if (position > -1) {
items!!.removeAt(position)
notifyItemRemoved(position)
}
}
fun isEmpty(): Boolean {
return itemCount == 0
}
fun setListener(listener: L) {
this.listener = listener
}
protected fun inflate(@LayoutRes layout: Int, parent: ViewGroup?, attachToRoot: Boolean): View {
return layoutInflater.inflate(layout, parent, attachToRoot)
}
protected fun inflate(@LayoutRes layout: Int, parent: ViewGroup?): View {
return inflate(layout, parent, false)
}
override fun setHasStableIds(hasStableIds: Boolean) {
super.setHasStableIds(hasStableIds)
}
}
Room is one of the popular database library using SQLite made by Google. IMO, Room has many advantages. It persists data over configuration changes, based on objected-oriented Modeling, supports migration, and it has really nice synergy with LiveData or Rxjava.
There are 3 major components in Room
- Entity: Represents a table within the database.
- Dao: Contains the methods used for accessing the database.
- Database: Contains the database holder and serves as the main access point for the underlying connection to your app's persisted, relational data.
In Project configuration section i've already showed how to add Room in your project. Here is the Entity, Dao and Database class for configuring the Room database in project
@Entity
data class Category(
@PrimaryKey
val id: String,
val categoryName: String,
val categoryDescription: String
)
@Dao
interface CategoryDao {
@Query("SELECT * FROM Category")
fun getAllCategory(): LiveData<List<Category>>
@Insert
fun insert(vararg category: Category)
@Delete
fun delete(category: Category)
@Insert
fun insertList(categoryList : List<Category>)
@Update
fun updateTodo(vararg category: Category)
}
@Database(entities = arrayOf(Category::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun categoryDao(): CategoryDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
private fun buildDatabase(context: Context) =
databaseBuilder(
context.applicationContext,
AppDatabase::class.java, "Sample.db").allowMainThreadQueries()
.build()
}
}
Please note that you can also customize your column name in Entity class if you want. All you have to do is to add @ColumnInfo(name = "desired_name")
before declaring a variable in Entity class. if you don't use this @ColumnInfo
annotation then it is automatically ignored.
This basically is a wrapper class which contains the business logic of our app. we will write all the database operation here so the person who is working on the UI does not have to worry about any database operations.
We have already created the Repository class here
class Repository(private var application: Application) : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
var mDao: CategoryDao
init {
mDao = AppDatabase.getInstance(application).categoryDao()
}
fun getAllCategory() = mDao.getAllCategory()
fun insertCategory(category: Category) {
launch { insertCategorySuspend(category) }
}
fun insertCategoryList(categoryList: List<Category>) {
launch { insertCategoryListSuspend(categoryList) }
}
private suspend fun insertCategorySuspend(category: Category) {
withContext(Dispatchers.IO) {
mDao.insert(category)
}
}
private suspend fun insertCategoryListSuspend(categoryList: List<Category>) {
withContext(Dispatchers.IO) {
mDao.insertList(categoryList)
}
}
}
In this Repository class, we will write all the DAO operations and access them in the UI through ViewModel.
Now we'll create a ViewModel class. We already discussed above about the ViewModel class.
We've already written our CategoryViewModel class
class CategoryViewModel(application: Application) : AndroidViewModel(application) {
var categoryLiveData: LiveData<List<Category>>? = null
var mRepository: Repository? = null
init {
mRepository = Repository(application)
categoryLiveData = mRepository?.getAllCategory()
}
internal fun getAllCategoryList(): LiveData<List<Category>>? {
return categoryLiveData
}
internal fun saveSampleData() {
mRepository?.insertCategoryList(getSampleList())
}
private fun getSampleList(): List<Category> {
val listOfCategory = ArrayList<Category>()
listOfCategory.add(Category(UUID.randomUUID().toString(), "Test 0", "Test 0"))
listOfCategory.add(Category(UUID.randomUUID().toString(), "Test 1", "Test 1"))
listOfCategory.add(Category(UUID.randomUUID().toString(), "Test 2", "Test 2"))
return listOfCategory
}
fun insertCategory(category: Category){
mRepository?.insertCategory(category)
}
fun checkValidity(name: String, description : String) : Boolean{
if(!TextUtils.isEmpty(name) && !TextUtils.isEmpty(description)){
return true
}
return false
}
}
So we've finaly approached to our last stage. Now we will access the methods we have written in our ViewModel class in our Activity class.
var mViewModel: CategoryViewModel? = null
mViewModel = ViewModelProviders.of(this).get(CategoryViewModel::class.java)
mViewModel?.getAllCategoryList()?.observe(this, Observer { data ->
// Do whatever you want to do with it right now, she is all your's now :p
})
For inserting the data in DB you can use something like this
var mViewModel: CategoryViewModel? = null
mViewModel = ViewModelProviders.of(this).get(CategoryViewModel::class.java)
mViewModel!!.insertCategory(
Category(
UUID.randomUUID().toString(),
alertDialogBuilder.et_name.text.toString(),
alertDialogBuilder.et_description.text.toString()
)
)
Here View is accessing the data from ViewModel class and ViewModel is accessing it from Repository class which has all the database operations.
Thanks for reading this ❤️. If I got something wrong please let me know about it. I would definitely love to improve more. While writing this i took a huge inspiration from this article published in https://medium.com