I'm seeing some odd behavior. I have a simple StateFlow<Boolean> in my ViewModel that is not being collected in the fragment. Definition:
private val _primaryButtonClicked = MutableStateFlow(false)
val primaryButtonClicked: StateFlow<Boolean> = _primaryButtonClicked
and here is where I set the value:
fun primaryButtonClick() {
_primaryButtonClicked.value = true
}
Here is where I'm collecting it.
repeatOnOwnerLifecycle {
launch(dispatchProvider.io()) {
freeSimPurchaseFragmentViewModel.primaryButtonClicked.collect {
if (it) {
autoCompletePlacesStateFlowModel.validateErrors()
formValidated = autoCompletePlacesStateFlowModel.validateAddress()
if (formValidated) {
freeSimPurchaseFragmentViewModel
.sumbitForm(autoCompletePlacesStateFlowModel.getStateFlowCopy())
}
}
}
}
}
repeatOnOwnerLifecycle:
inline fun Fragment.repeatOnOwnerLifecycle(
state: Lifecycle.State = Lifecycle.State.RESUMED,
crossinline block: suspend CoroutineScope.() -> Unit
) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(state) {
block()
}
}
What am I doing wrong? The collector never fires.
Does this make sense?
val primaryButtonClicked: StateFlow<Boolean> = _primaryButtonClicked.asStateFlow()
Also I couldn't understand the inline function part, because under the hood seems you wrote something like this
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch(dispatchProvider.io()) {
freeSimPurchaseFragmentViewModel.primaryButtonClicked.collect {
if (it) {
autoCompletePlacesStateFlowModel.validateErrors()
formValidated = autoCompletePlacesStateFlowModel.validateAddress()
if (formValidated) {
freeSimPurchaseFragmentViewModel
.sumbitForm(autoCompletePlacesStateFlowModel.getStateFlowCopy())
}
}
}
}
}
}
Why are you launching one coroutine in another and collect the flow from IO dispatcher? You need to collect the values from the main dispatcher.
Related
So I have a ViewModel I'm trying to unit test. It is using the stateIn operator. I found this documentation about how to test stateflows created using the stateIn operator https://developer.android.com/kotlin/flow/test but the mapLatest never triggers even though I'm collecting the flow.
class DeviceConfigurationViewModel(
val systemDetails: SystemDetails,
val step: AddDeviceStep.ConfigureDeviceStep,
val service: DeviceRemoteService
) : ViewModel(), DeviceConfigurationModel {
#OptIn(ExperimentalCoroutinesApi::class)
private val _state: StateFlow<DeviceConfigurationModel.State> =
service.state
.mapLatest { state ->
when (state) {
DeviceRemoteService.State.Connecting -> {
DeviceConfigurationModel.State.Connecting
}
is DeviceRemoteService.State.ConnectedState.Connected -> {
state.sendCommand(step.toCommand(systemDetails))
DeviceConfigurationModel.State.Connected
}
is DeviceRemoteService.State.ConnectedState.CommandSent -> {
DeviceConfigurationModel.State.Configuring
}
is DeviceRemoteService.State.ConnectedState.MessageReceived -> {
transformMessage(state)
}
is DeviceRemoteService.State.Disconnected -> {
transformDisconnected(state)
}
}
}
.distinctUntilChanged()
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000), // Keep it alive for a bit if the app is backgrounded
DeviceConfigurationModel.State.Disconnected
)
override val state: StateFlow<DeviceConfigurationModel.State>
get() = _state
private fun transformDisconnected(
state: DeviceRemoteService.State.Disconnected
): DeviceConfigurationModel.State {
return if (state.hasCause) {
DeviceConfigurationModel.State.UnableToConnect(state)
} else {
state.connect()
DeviceConfigurationModel.State.Connecting
}
}
private fun transformMessage(state: DeviceRemoteService.State.ConnectedState.MessageReceived): DeviceConfigurationModel.State {
return when (val message = state.message) {
is Message.AddedToProject -> DeviceConfigurationModel.State.Configured
is Message.ConfigWifiMessage -> {
if (!message.values.success) {
DeviceConfigurationModel.State.Error(
message.values.errorCode,
state,
step.toCommand(systemDetails)
)
} else {
DeviceConfigurationModel.State.Configuring
}
}
}
}
}
And here's my unit test. The mapLatest never seems to get triggered even though I'm collecting the flow. I'm using the suggestions here https://developer.android.com/kotlin/flow/test
#OptIn(ExperimentalCoroutinesApi::class)
class DeviceConfigurationViewModelTest {
private val disconnectedService = mock<DisconnectedService>()
private val deviceServiceState: MutableStateFlow<DeviceRemoteService.State> =
MutableStateFlow(DeviceRemoteService.State.Disconnected(disconnectedService, Exception()))
private val deviceService = mock<DeviceRemoteService> {
on { state } doReturn deviceServiceState
}
private val systemDetails = mock<SystemDetails> {
on { controllerAddress } doReturn "192.168.1.112"
on { controllerName } doReturn "000FFF962FE7"
}
private val step = AddDeviceDeviceStep.ConfigureDeviceStep(
44,
"Thou Shalt Not Covet Thy Neighbor’s Wifi",
"testing616"
)
private lateinit var viewModel: DeviceConfigurationViewModel
#Before
fun setup() {
viewModel = DeviceConfigurationViewModel(systemDetails, step, deviceService)
}
#Test
fun testDeviceServiceDisconnectWithCauseMapsToUnableToConnect() =
runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.state.collect() }
deviceServiceState.emit(
DeviceRemoteService.State.Disconnected(Exception("Something bad happened"))
)
assertThat(viewModel.state.value).isInstanceOf(DeviceConfigurationModel.State.UnableToConnect::class.java)
collectJob.cancel()
}
}
I believe this is happening because the viewModelScope uses a hardcoded Main dispatcher under the hood.
You can follow the instructions here in the Android documentation to see how you can to set the Main dispatcher for tests.
I'm trying to insert separators to my list using the paging 3 compose library however, insertSeparators doesn't seem to indicate when we are at the beginning or end. My expectations are that before will be null at the beginning while after will be null at the end of the list. But it's never null thus hard to know when we are at the beginning or end. Here is the code:
private val filterPreferences =
MutableStateFlow(HomePreferences.FilterPreferences())
val games: Flow<PagingData<GameModel>> = filterPreferences.flatMapLatest {
useCase.execute(it)
}.map { pagingData ->
pagingData.map { GameModel.GameItem(it) }
}.map {
it.insertSeparators {before,after->
if (after == null) {
return#insertSeparators null
}
if (before == null) {
Log.i(TAG, "before is null: ") // never reach here
return#insertSeparators GameModel.SeparatorItem("title")
}
if(condition) {
GameModel.SeparatorItem("title")
}
else null
}
}
.cachedIn(viewModelScope)
GamesUseCase
class GamesUseCase #Inject constructor(
private val executionThread: PostExecutionThread,
private val repo: GamesRepo,
) : FlowUseCase<HomePreferences, PagingData<Game>>() {
override val dispatcher: CoroutineDispatcher
get() = executionThread.io
override fun execute(params: HomePreferences?): Flow<PagingData<Game>> {
val preferences = params as HomePreferences.FilterPreferences
preferences.apply {
return repo.fetchGames(query,
parentPlatforms,
platforms,
stores,
developers,
genres,
tags)
}
}
}
FlowUseCase
abstract class FlowUseCase<in Params, out T>() {
abstract val dispatcher: CoroutineDispatcher
abstract fun execute(params: Params? = null): Flow<T>
operator fun invoke(params: Params? = null) = execute(params).flowOn(dispatcher)
}
Here is the dependency :
object Pagination {
object Version {
const val pagingCompose = "1.0.0-alpha14"
}
const val pagingCompose = "androidx.paging:paging-compose:${Version.pagingCompose}"
}
I'm assuming that filterPreferences gives you Flow of some preference and useCase.execute returns Flow<PagingData<Model>>, correct?
I believe that the problem is in usage of flatMapLatest - it mixes page events of multiple useCase.execute calls together.
You should do something like this:
val games: Flow<Flow<PagingData<GameModel>>> = filterPreferences.mapLatest {
useCase.execute(it)
}.mapLatest {
it.map { pagingData -> pagingData.map { GameModel.GameItem(it) } }
}.mapLatest {
it.map { pagingData ->
pagingData.insertSeparators { before, after -> ... }
} // .cachedIn(viewModelScope)
}
This same structure works for us very well. I'm only not sure how cachedIn will work here, we are using a different caching mechanism, but you can try.
How can I call a composable function from context of corrutines?
I trying the following code but I getting the error.
#Composable
fun ShowItems(){
var ListArticle = ArrayList<Article>()
lifecycleScope.launchWhenStarted {
// Triggers the flow and starts listening for values
viewModel.uiState.collect { uiState ->
// New value received
when (uiState) {
is MainViewModel.LatestNewsUiState.Success -> {
//Log.e(TAG,"${uiState.news}")
if(uiState.news != null){
for(i in uiState.news){
ListArticle.add(i)
}
context.ItemNews(uiState.news.get(4))
Log.e(TAG,"${uiState.news}")
}
}
is MainViewModel.LatestNewsUiState.Error -> Log.e(TAG,"${uiState.exception}")
}
}
}
}
You should do something like this:
#Composable
fun ShowItems(){
val uiState = viewModel.uiState.collectAsState()
// Mount your UI in according to uiState object
when (uiState.value) {
is MainViewModel.LatestNewsUiState.Success -> { ... }
is MainViewModel.LatestNewsUiState.Error -> { ... }
}
// Launch a coroutine when the component is first launched
LaunchedEffect(viewModel) {
// this call should change uiState internally in your viewModel
viewModel.loadYourData()
}
}
My project has a lot of operations that must be performed one after another. I was using listeners, but I found this tutorial Kotlin coroutines on Android and I wanted to change my sever call with better readable code. But I think I am missing something. The below code always return an error from getTime1() function:
suspend fun getTimeFromServer1() :ResultServer<Long> {
val userId = SharedPrefsHelper.getClientId()
return withContext(Dispatchers.IO) {
val call: Call<ResponseFromServer>? = userId?.let { apiInterface.getTime(it) }
(call?.execute()?.body())?.run {
val time:Long? = this.data?.time
time?.let {
Timber.tag("xxx").e("time received it ${it}")// I am getting the right result here
ResultServer.Success(it)
}
Timber.tag("xxx").e("time received ${time}")
}
ResultServer.Error(Exception("Cannot get time"))
}
}
fun getTime1() {
GlobalScope.launch {
when (val expr: ResultServer<Long> = NetworkLayer.getTimeFromServer1()) {
is ResultServer.Success<Long> -> Timber.tag("xxx").e("time is ${expr.data}")
is ResultServer.Error -> Timber.tag("xxx").e("time Error") //I am always get here
}}
}
}
But if I am using listeners (getTime()) everything works perfectly:
suspend fun getTimeFromServer(savingFinishedListener: SavingFinishedListener<Long>) {
val userId = SharedPrefsHelper.getClientId()
withContext(Dispatchers.IO) {
val call: Call<ResponseFromServer>? = userId?.let { apiInterface.getTime(it) }
(call?.execute()?.body())?.run {
val time:Long? = this.data?.time
time?.let {
Timber.tag("xxx").e("time received it ${it}")
savingFinishedListener.onSuccess(it)
}
}
savingFinishedListener.onSuccess(null)
}
}
fun getTime() {
GlobalScope.launch {
NetworkLayer.getTimeFromServer(object:SavingFinishedListener<Long>{
override fun onSuccess(t: Long?) {
t?.let {
Timber.tag("xxx").e("time here $it") //I am getting the right result
}
}
})
}
}
Thanks in advance for any help.
The last line of a lambda is implicitly the return value of that lambda. Since you don't have any explicit return statements in your withContext lambda, its last line:
ResultServer.Error(Exception("Cannot get time"))
means that it always returns this Error. You can put return#withContext right before your ResultServer.Success(it) to make that line of code also return from the lambda.
Side note: don't use GlobalScope.
I am trying to use the following code:
suspend fun <T> SavedStateHandle.getStateFlow(
key: String,
initialValue: T? = get(key)
): MutableStateFlow<T?> = this.let { handle ->
withContext(Dispatchers.Main.immediate) {
val liveData = handle.getLiveData<T?>(key, initialValue).also { liveData ->
if (liveData.value === initialValue) {
liveData.value = initialValue
}
}
val mutableStateFlow = MutableStateFlow(liveData.value)
val observer: Observer<T?> = Observer { value ->
if (value != mutableStateFlow.value) {
mutableStateFlow.value = value
}
}
liveData.observeForever(observer)
mutableStateFlow.also { flow ->
flow.onCompletion {
withContext(Dispatchers.Main.immediate) {
liveData.removeObserver(observer)
}
}.onEach { value ->
withContext(Dispatchers.Main.immediate) {
if (liveData.value != value) {
liveData.value = value
}
}
}.collect()
}
}
}
I am trying to use it like so:
// in a Jetpack ViewModel
var currentUserId: MutableStateFlow<String?>
private set
init {
runBlocking(viewModelScope.coroutineContext) {
currentUserId = state.getStateFlow("currentUserId", sessionManager.chatUserFlow.value?.uid)
// <--- this line is never reached
}
}
UI thread freezes. I have a feeling it's because of collect() as I'm trying to create an internal subscription managed by the enclosing coroutine context, but I also need to get this StateFlow as a field. There's also the cross-writing of values (if either changes, update the other if it's a new value).
Overall, the issue seems to like on that collect() is suspending, as I never actually reach the line after getStateFlow().
Does anyone know a good way to create an "inner subscription" to a Flow, without ending up freezing the surrounding thread? The runBlocking { is needed so that I can synchronously assign the value to the field in the ViewModel constructor. (Is this even possible within the confines of 'structured concurrency'?)
EDIT:
// For more details, check: https://gist.github.com/marcellogalhardo/2a1ec56b7d00ba9af1ec9fd3583d53dc
fun <T> SavedStateHandle.getStateFlow(
scope: CoroutineScope,
key: String,
initialValue: T
): MutableStateFlow<T> {
val liveData = getLiveData(key, initialValue)
val stateFlow = MutableStateFlow(initialValue)
val observer = Observer<T> { value ->
if (value != stateFlow.value) {
stateFlow.value = value
}
}
liveData.observeForever(observer)
stateFlow.onCompletion {
withContext(Dispatchers.Main.immediate) {
liveData.removeObserver(observer)
}
}.onEach { value ->
withContext(Dispatchers.Main.immediate) {
if (liveData.value != value) {
liveData.value = value
}
}
}.launchIn(scope)
return stateFlow
}
ORIGINAL:
You can piggyback over the built-in notification system in SavedStateHandle, so that
val state = savedStateHandle.getLiveData<State>(Key).asFlow().shareIn(viewModelScope, SharingStarted.Lazily)
...
savedStateHandle.set(Key, "someState")
The mutator happens not through methods of MutableLiveData, but through the SavedStateHandle that will update the LiveData (and therefore the flow) externally.
I am in a similar position, but I do not want to modify the value through the LiveData (as in the accepted solution). I want to use only flow and leave LiveData as an implementation detail of the state handle.
I also did not want to have a var and initialize it in the init block. I changed your code to satisfy both of these constraints and it does not block the UI thread. This would be the syntax:
val currentUserId: MutableStateFlow<String?> = state.getStateFlow("currentUserId", viewModelScope, sessionManager.chatUserFlow.value?.uid)
I provide a scope and use it to launch a coroutine that handles flow's onCompletion and collection. Here is the full code:
fun <T> SavedStateHandle.getStateFlow(
key: String,
scope: CoroutineScope,
initialValue: T? = get(key)
): MutableStateFlow<T?> = this.let { handle ->
val liveData = handle.getLiveData<T?>(key, initialValue).also { liveData ->
if (liveData.value === initialValue) {
liveData.value = initialValue
}
}
val mutableStateFlow = MutableStateFlow(liveData.value)
val observer: Observer<T?> = Observer { value ->
if (value != mutableStateFlow.value) {
mutableStateFlow.value = value
}
}
liveData.observeForever(observer)
scope.launch {
mutableStateFlow.also { flow ->
flow.onCompletion {
withContext(Dispatchers.Main.immediate) {
liveData.removeObserver(observer)
}
}.collect { value ->
withContext(Dispatchers.Main.immediate) {
if (liveData.value != value) {
liveData.value = value
}
}
}
}
}
mutableStateFlow
}