MVVM+ Room + Factory + Coroutines in kotlin

MVVM+ Room + Factory + coroutine in kotlin

MVVM application architecture

As a quick definition, it consists of several layers

The DAO layer, which is responsible for the commands to communicate with the SQL database from reading and writing.

The other layer is the Repository, which is an optional layer, except in applications that contain more than one data source

„More than a database, servers, and API“, which represents the repository that feeds data to the application.

As for the last layer, which is the ViewModel, it was designed to stick to the UI such as the Activity and the Fragment with awareness of their life cycle and providing it with data from the repository „or from the DAO if we neglect the repository layer“

MVVM code serialization

• Put the dependencies for the Room in the Build.Gradle file.

• Put the dependencies for the Coroutines in the Build.Gradle file.

• Creating the Model or rather the Entity to represent the data.

• Creation of the Dao to represent SQL commands.

Do not forget to tag these functions with suspend, and the return type for them is either encapsulated in LiveData or Flow. Also, do not forget to configure it to use paging in case we need that.

• Creation of the Room database.

Do not forget to fill it in with data in advance if we want to do so.

• Write and perform tests that relate to the database.

• Create a Repository to represent the data warehouse.

It will usually be optional, and necessary if we have more than one database or other data source

„Although the best thing is to make all data sources flow into the Room base and we only read from it.”

Create a ViewModel to represent the connection between the database and the user interface.

• Don't forget to put the light Logic codes that relate to the interface or the data modification codes.

• And don't forget to put switch ciphers from one thread to another, whether it's Threads, Executors, or Coroutines.

• Also use its own scope viewModelScope or create a custom scope.

• Store and display data in the user interface.

o Whether the Adapter creates a binding to the RecyclerView.

o Or creating views represented as buttons for saving, modifying, deleting, and querying the database.

I will touch on points related to the Dao, the ViewModel, and the Activity/Fragment.

Dao class with coroutines

In this layer, we will see several examples, including two examples of using LiveData as a review, then two examples of using Flow from Cortinas, and finally three examples of using the word suspend to make functions delay.

// LiveData

@Query("SELECT * FROM note_table WHERE id = :noteId")

fun getNote(noteId: String): LiveData<NoteEntity?>

• The first example: We used the LiveData to return only one element. Note that this element will be a Safe Call Operator using a question symbol, which may return a null element that does not crash the program.

// LiveData

@Query("SELECT * FROM note_table")

fun getAllNotes(): LiveData<List<NoteEntity>>

• The second example: Similar to the first example, but here we return a full string of elements, and if it does not exist, it will return an empty string.

• Be aware that LiveData enables the function to run in a back-end computer thread. In order to extract the data from it, we must monitor it in an Observer in one of the objects that has a life cycle, such as the Activity and the Fragment, otherwise, we will have to use observeForever(), taking into account its termination, otherwise a memory leak will occur, and do not forget that we can create a LifeCycle for any class we want another eyeliner.

• Be aware that LiveData is for light queries, but for complex and nested queries, we can use MediatorLiveData.

// Flow

@Query("SELECT * FROM note_table WHERE id = :noteId")

fun getNote(noteId: String): Flow<NoteEntity?>

• The third example: We used Coroutines, especially the type of flow, including Flow (and to return one element, as in the first example, we will use this flow to extract one result from it, through the function first() later, which allows us to get the first result of the flow and then close it.

// Flow

@Query("SELECT * FROM note_table")

fun getAllNotes(): Flow<List<NoteEntity>>

Fourth example: Similar to the third example, but here we want to get a complete string of elements. So we use the collect() function later, or convert it to LiveData and monitor it.

// suspend

@Insert(onConflict = OnConflictStrategy.REPLACE)

suspend fun insert(note: NoteEntity)

// suspend

@Insert(onConflict = OnConflictStrategy.REPLACE)

suspend fun update(note: NoteEntity)

// suspend

@Delete

suspend fun delete(note: NoteEntity)

• Other examples: Here we have finished with the SQL commands for the query that comes with the @Query notation. We have other commands left in the remaining examples, which represent storage, update, and deletion. Here we can use the word suspend with it to make it a function to delay it running in the background so that it does not slow us down

Note that you can make these functions return a result. For example, the delete and update functions can return an int element for the number of rows affected by it. The Insert function can return a Long element as the ID number of the saved field. • In the end, you can dispense with LiveData and use Flow.

• We will see how to use them and extract data from them in the next layers.

ViewModel class with Coroutines

Note: I skipped the Repository layer because it is an optional layer in many cases, unless we have more than one data source, where it is necessary. Sometimes you will need to create a LifeCycle and Scope for proteins and so on.

While the ViewModel comes with a LifeCycle attached to the interface, whether Activity or Fragment, it also comes with its own Scope called ViewModelScope ready to use. If you don't want all of these things, it will be an iterative layer for the ViewModel and it is useless. In these examples, we will receive the functions that we defined in the Dao file in the previous section.

As for passing inputs to the ViewModel, by default you will need another Factory class to inject these inputs. For example, the entry of the Application type, which represents the context scope, or as the database entry, which is represented as Dao, or even as special entries such as the IDs of the elements, and so on. An example for Factory to pass only two entries, Application and Dao, is as follows:

class MyViewModelFactory(
    private val dataSource: NoteDao,
    private val application: Application,
    private val noteId: Long
    ) : ViewModelProvider.Factory {
    @Suppress("unchecked_cast")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
            return MyViewModel(dataSource, application, noteId) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

The Factory functions as a primitive injection method for the ViewModel.

We can also use dependency injection libraries such as the Dagger library previously, and currently it is recommended to use it officially is the Hilt library, which will be considered among the Jetpack libraries later in the final releases. I will update this article when it is available for end use if possible.

Now we come to the ViewModel examples:

class MyViewModel(
    val database: NoteDao,
    application: Application,
    noteId: Long
) : AndroidViewModel(application) {
    private var notes = database.getAllNotes()
    private var note = database.getNote(noteId)
...

• To receive elements from functions that return LiveData, all we have to do is call them.

• Don't worry, it runs itself in the background and is aware of the latest data updates thanks to the features of LiveData.

And to receive elements from functions that return Flow is easier. All we have to do is create a delay function using the suspend word as follows:

...
    suspend fun getAllNotes(): Flow<List<NoteEntity>> {
        return database.getAllNotes()
    }

    suspend fun getNote(noteId: Long): NoteEntity {
        return database.getNote(noteId).first()
    }
...

• Since the return type in Dao is Flow, and since Flow is considered a routine/delay function, we must call it from within another routine or from within a deferred function. So we made these functions suspend.

• In the first example, we called it from within a deferred function. We will extract data from it in the interface later.

• In the second example, we did as it came in the first example, but we increased the use of another deferred function, which is first(), where we were able to query the first result and then close this flow. Later we can extract this statement in the interface through a routine. Note that the referenced output is a Note element, not a Stream.

And now for the other functions responsible for saving, updating and deleting. We receive them through a delay function running in a back-end computer thread. Then we will use this function in a routine that runs in the main computer thread. So it's clean when we come to deal with them in the UI layer. It is a way out of the rabbit hole (crotinas). The following example we will apply to the save function and cut the rest of the functions:

...
    private suspend fun insert(note: NoteEntity) {
        viewModelScope.launch(Dispatchers.IO) {
            database.insert(note)
        }
    }

    fun save(note: NoteEntity) {
        viewModelScope.launch(Dispatchers.Main) {
            insert(note)
        }
    }
...

The first function, insert(), is considered a delay function through the word suspend (ie routine) and works in the background through Dispatchers.IO (

• Also note the use of the Scope that comes with the ViewModel in its package instead of creating our own

• As for the second function, it is considered a normal function, that is, it is not a routine, but it creates a routine that works in the main thread, and its function is to call the previous deferred function (by virtue of the fact that we can only call a deferred function through a routine or another function). Thus, we are out of the rabbit hole and we can call our save function anywhere we want to save Note items.

An interface layer, Activity or Fragment, to extract and display data

In the end, when we get to the layers of the application interface, such as the Activity or the Fragment, we can create routines, for example using the launch constructor to run the deferred functions that we wrote in the previous layers and extract the result for us in the main UI thread so that we can display it to the user.

But first, we will need to create a scope for this interface, whether Activity or Fragment, by creating a Job element for it and then a Scope, which will consist of the Job and the Dispatcher located in the Despatcher.Main thread to run the routines as follows:

 class Activity/Fragment
...
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private lateinit var myViewModel: MyViewModel
...

• First we create an element of type Job to control this scope to close or call the routines.

Then we create our uiScope, which we want to move and run routines in the main thread.

• And do not forget to terminate the Job in functions that end at the end of the life cycle, such as onDestroy or onDestroyView in the case of the Fragment.

To initialize the ViewModel's Init through the Factory and give it the inputs as follows:

...
val application = requireNotNull(this.activity).application
val dataSource = AppDatabase.getInstance(application).noteDao
val viewModelFactory = MyViewModelFactory(dataSource, application, 1)
myViewModel = ViewModelProvider(this, viewModelFactory).get(MyViewModel::class.java)
...

To extract data for LiveData we need to monitor them by creating an observer like this:

...
myViewModel.getAllNotes().observe(viewLifecycleOwner, Observer {
    it?.let { 
        // it: List<NoteEntity>
    }
})
...

As for the Flow, we will need to monitor it through the collect() function inside two routines, because we can only call two routines from within another routine, as follows:

...
uiScope.launch {
    startFragmentViewModel.getAllNotes().collect {
        // it: List<NoteEntity>
    }
}
...

• We create this routine in the uiScope we created earlier, which allows us to receive elements into the main thread.

And in the case of using the first() function, we will get the individual element directly like this:

...
uiScope.launch {
   val note: NoteEntitiy = myViewModel.getNote(1)
}
...

This table shows the functions in classes

Dao

ViewModel

Fragment أو Activity

• We encapsulate the return type of some of these functions in Flow, so that this function becomes a routine.

• We create our own scope or use its own scope viewModelScope.

• We create a special scope for the interface that works in the main computer thread through the distributor Main.