Correct flow between Kotlin, Realm and ViewModels using Coroutines

Viewed 1393

Disclaimer: This is not actual code from any app, but an example of the flow and my current understanding on how best to do this. I am looking for help improving upon or what I am doing wrong.

I am trying to figure out the best way to structure an android application using the new jetpack viewModels, realm, and coroutines. I put together a gist of the flow that I have so far, and would love some feedback on how I can improve, what I could change, or what I am doing wrong. Ideally with examples or direct changes to my code.

It works as is, I am just not sure if I am using coroutines correctly or efficiently, and if there is a better way to structure the DAO's so that Realm can be injected for better testability. Someone has already mentioned changing the DAO to extend the LiveData<>, and using onActive() and onInactive() for posting the object. Is that a good idea?

// About Model is the model used by Realm. These models contains realm specific types, like RealmList
open class AboutModel(
    var name: String = "",
    @PrimaryKey
    var version: String = ""
): RealmObject() {

    /**
     * Conversion function, to convert the view model layer object to the data layer object
     */
    companion object {
        fun from(about: About): AboutModel = AboutModel(about.name, about.version)
    }

    fun toObject(): About =
        About(
            this.name,
            this.version
        )
}
// About class used everywhere outside of the data/realm layer. 
// Lines up with the AboutModel class, but free of realm or any other database specific types.
// This way, realm objects are not being referenced anywhere else. In case I ever need to
// replace realm for something else.
class About (val name: String = "Test", val version: String = "1.0.0") {

    override fun toString(): String {
        return "author is : $name, version is: $version"
    }
}
// Couldn't inject the realm instance because its thread would not match with a suspend function. 
// Even if both where background threads. Would be better if I could inject it, but couldn't get
// that to work.
class AboutDao() {
    private val _about = MutableLiveData<About>()

    init {
        val realm = Realm.getDefaultInstance()
        val aboutModel = realm.where(AboutModel::class.java).findFirst()
        _about.postValue(aboutModel?.toObject() ?: About())
        realm.close()
    }

    suspend fun setAbout(about: About) = withContext(Dispatchers.IO) {
        val realm: Realm = Realm.getDefaultInstance()
        realm.executeTransaction {
            realm.copyToRealmOrUpdate(AboutModel.from(about))
            _about.postValue(about)
        }
        realm.close()
    }

    fun getAbout() = _about as LiveData<About>
}
// Database is a singleton instance, so there is only ever one instance of the DAO classes
class Database private constructor() {
    
    var aboutDao = AboutDao()
        private set

    companion object {
        // @Volatile - Writes to this property are immediately visible to other threads
        @Volatile private var instance: Database? = null

        suspend fun getInstance() = withContext(Dispatchers.IO) {
            return@withContext instance ?: synchronized(this) {
                instance ?: Database().also { instance = it }
            }
        }
    }
}
// Repo maintains the dao access. Is also setup to run as a singleton
class AboutRepo private constructor(private val aboutDao: AboutDao){

    // This may seem redundant.
    // Imagine a code which also updates and checks the backend.
    suspend fun set(about: About) {
        aboutDao.setAbout(about)
    }

    suspend fun getAbout() = aboutDao.getAbout()

    companion object {
        // Singleton instantiation you already know and love
        @Volatile private var instance: AboutRepo? = null

        fun getInstance(aboutDao: AboutDao) =
            instance ?: synchronized(this) {
                instance ?: AboutRepo(aboutDao).also { instance = it }
            }
    }
}
// Injector is used to help keep the injection in a single place for the fragments and activities.
object Injector {

    // This will be called from About Fragment
    suspend fun provideAboutViewModelFactory(): AboutViewModelFactory = withContext(Dispatchers.Default) {
        AboutViewModelFactory(getAboutRepo())

    }

    private suspend fun getAboutRepo() = withContext(Dispatchers.IO) {
        AboutRepo.getInstance(Database.getInstance().aboutDao)
    }

}
// AboutViewModel's Factory. I found this code online, as a helper for injecting into the viewModel's factory.
class AboutViewModelFactory (private val aboutRepo: AboutRepo)
    : ViewModelProvider.NewInstanceFactory() {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return AboutViewModel(aboutRepo) as T
    }
}
// About Fragments ViewModel
class AboutViewModel(private val aboutRepo: AboutRepo) : ViewModel() {

    suspend fun getAbout() = aboutRepo.getAbout()
    suspend fun setAbout(about: About) = aboutRepo.set(about)

}
// Fragment's onActivityCreated, I set the viewModel and observe the model from the view model for changes
override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        lifecycleScope.launch {

            viewModel = ViewModelProviders.of(
                    this@AboutFragment,
                    Injector.provideAboutViewModelFactory()
            ).get(AboutViewModel::class.java)


            withContext(Dispatchers.Main) {
                viewModel.getAbout().observe(viewLifecycleOwner, Observer { about ->
                    version_number.text = about?.version
                })
            }
        }
    }
0 Answers
Related