How to convert a Flow<CustomType> to StateFlow<UIState>? - Android Kotlin

Viewed 638

I am learning Android development, and as I saw in many topics, people were talking about that LiveData is not recommended to use anymore. I mean it's not up-to-date, and we should use Flows instead.

I am trying to get data from ROOM database with Flows and then convert them to StateFlow because as I know they are observables, and I also want to add UI states to them. Like when I get data successfully, state would change to Success or if it fails, it changes to Error.

I have a simple app for practicing. It stores subscribers with name and email, and show them in a recyclerview.

I've checked a lot of sites, how to use stateIn method, how to use StateFlows and Flows but didn't succeed. What's the most optimal way to do this?

And also what's the proper way of updating recyclerview adapter? Is it okay to change it all the time in MainActivity to a new adapter?

Here is the project (SubscriberViewModel.kt - line 30): Project link

If I am doing other stuff wrong, please tell me, I want to learn. I appreciate any kind of help.

DAO:

import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Dao
interface SubscriberDAO {

@Insert
suspend fun insertSubscriber(subscriber : Subscriber) : Long

@Update
suspend fun updateSubscriber(subscriber: Subscriber) : Int

@Delete
suspend fun deleteSubscriber(subscriber: Subscriber) : Int

@Query("DELETE FROM subscriber_data_table")
suspend fun deleteAll() : Int

@Query("SELECT * FROM subscriber_data_table")
fun getAllSubscribers() : Flow<List<Subscriber>>

@Query("SELECT * FROM subscriber_data_table WHERE :id=subscriber_id")
fun getSubscriberById(id : Int) : Flow<Subscriber>

}

ViewModel:

class SubscriberViewModel(private val repository: SubscriberRepository) : ViewModel() {

private var isUpdateOrDelete = false
private lateinit var subscriberToUpdateOrDelete: Subscriber

val inputName = MutableStateFlow("")
val inputEmail = MutableStateFlow("")

private val _isDataAvailable = MutableStateFlow(false)
val isDataAvailable : StateFlow<Boolean>
    get() = _isDataAvailable

val saveOrUpdateButtonText = MutableStateFlow("Save")
val deleteOrDeleteAllButtonText = MutableStateFlow("Delete all")

/*
//TODO - How to implement this as StateFlow<SubscriberListUiState> ??
//private val _subscribers : MutableStateFlow<SubscriberListUiState>
//val subscribers : StateFlow<SubscriberListUiState>
    get() = _subscribers
*/

private fun clearInput() {
    inputName.value = ""
    inputEmail.value = ""
    isUpdateOrDelete = false
    saveOrUpdateButtonText.value = "Save"
    deleteOrDeleteAllButtonText.value = "Delete all"
}

fun initUpdateAndDelete(subscriber: Subscriber) {
    inputName.value = subscriber.name
    inputEmail.value = subscriber.email
    isUpdateOrDelete = true
    subscriberToUpdateOrDelete = subscriber
    saveOrUpdateButtonText.value = "Update"
    deleteOrDeleteAllButtonText.value = "Delete"
}

fun saveOrUpdate() {
    if (isUpdateOrDelete) {
        subscriberToUpdateOrDelete.name = inputName.value
        subscriberToUpdateOrDelete.email = inputEmail.value
        update(subscriberToUpdateOrDelete)
    } else {
        val name = inputName.value
        val email = inputEmail.value
        if (name.isNotBlank() && email.isNotBlank()) {
            insert(Subscriber(0, name, email))
        }
        inputName.value = ""
        inputEmail.value = ""
    }
}

fun deleteOrDeleteAll() {
    if (isUpdateOrDelete) {
        delete(subscriberToUpdateOrDelete)
    } else {
        deleteAll()
    }
}

private fun insert(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
    repository.insert(subscriber)
    _isDataAvailable.value = true
}

private fun update(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
    repository.update(subscriber)
    clearInput()
}

private fun delete(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
    repository.delete(subscriber)
    clearInput()
}

private fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
    repository.deleteAll()
    //_subscribers.value = SubscriberListUiState.Success(emptyList())
    _isDataAvailable.value = false
}

sealed class SubscriberListUiState {
    data class Success(val list : List<Subscriber>) : SubscriberListUiState()
    data class Error(val msg : String) : SubscriberListUiState()
}
}

MainActivity:

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: SubscriberViewModel
private lateinit var viewModelFactory: SubscriberViewModelFactory

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
    val dao = SubscriberDatabase.getInstance(application).subscriberDAO
    viewModelFactory = SubscriberViewModelFactory(SubscriberRepository(dao))
    viewModel = ViewModelProvider(this, viewModelFactory)[SubscriberViewModel::class.java]
    binding.viewModel = viewModel
    binding.lifecycleOwner = this
    initRecycleView()
}

private fun initRecycleView() {
    binding.recyclerViewSubscribers.layoutManager = LinearLayoutManager(
        this@MainActivity,
        LinearLayoutManager.VERTICAL, false
    )
    displaySubscribersList()
}

private fun displaySubscribersList() {
    /*
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.subscribers.collect { uiState ->
                when (uiState) {
                    is SubscriberViewModel.SubscriberListUiState.Success -> {
                        binding.recyclerViewSubscribers.adapter = SubscriberRecyclerViewAdapter(uiState.list) {
                            subscriber: Subscriber -> listItemClicked(subscriber)
                        }
                    }
                    is SubscriberViewModel.SubscriberListUiState.Error -> {
                        Toast.makeText(applicationContext,uiState.msg, Toast.LENGTH_LONG).show()
                    }
                }
            }
        }
    }*/
}

private fun listItemClicked(subscriber: Subscriber) {
    Toast.makeText(this, "${subscriber.name} is selected", Toast.LENGTH_SHORT).show()
    viewModel.initUpdateAndDelete(subscriber)
}

}
1 Answers

You can convert a Flow type into a StateFlow by using stateIn method.

 private val coroutineScope = CoroutineScope(Job())
 private val flow: Flow<CustomType>
 val stateFlow = flow.stateIn(scope = coroutineScope)

In order to transform the CustomType into UIState, you can use the transformLatest method on Flow. It will be something like below:

stateFlow.transformLatest { customType ->
    customType.toUiState()
}

Where you can create an extension function to convert CustomType to UiState like this:

fun CustomType.toUiState() = UiState(
x = x,
y = y... and so on.
)
Related