0

I have this sealed class:

sealed class Resource<out T> {
    object Loading: Resource<Nothing>()
    data class Success<out T>(val data: T): Resource<T>()
    data class Failure(val message: String): Resource<Nothing>()
}

In the repository class I have this function that deletes an item from an API:

override suspend fun deleteItem(id: String) = flow {
    try {
        emit(Resource.Loading)
        emit(Resource.Success(itemsRef.document(id).delete().await()))
    } catch (e: Exception) {
        emit(Resource.Failure(e.message))
    }
}

The result of the delete operation is Void?. Now, in the ViewModel class I declare:

val state = mutableStateOf<Resource<Void?>>(Success(null))

And update it when the delete completes:

fun deleteItem(id: String) {
    viewModelScope.launch {
        repo.deleteItem(id).collect { response ->
            state.value = response
        }
    }
}

I have created a Card and inside onClick I have added:

IconButton(
    onClick = viewModel.deleteItem(id),
)

Which actually deletes that item form database correctly. But I cannot track the result of the operation. I tried using:

when(val res = viewModel.state.value) {
    is Resource.Loading -> Log.d(TAG, "Loading")
    is Resource.Success -> Log.d(TAG, "Success")
    is Resource.Failure -> Log.d(TAG, "Failure")
}

But only the case Loading is triggered. No success/failure at all. What can be wrong here? As it really acts like a synchronous operation.

Joan P.
  • 2,368
  • 6
  • 30
  • 63

2 Answers2

3

I've tested your approach without a repository, and compose part looks totally fine:

var i = 0

@Composable
fun TestScreen(viewModel: TestViewModel = viewModel()) {
    val state by viewModel.state
    Text(
        when (val stateSmartCast = state) {
            is Resource.Failure -> "Failure ${stateSmartCast.message}"
            Resource.Loading -> "Loading"
            is Resource.Success -> "Success ${stateSmartCast.data}"
        }
    )
    Button(onClick = {
        viewModel.deleteItem(++i)
    }) {

    }
}

class TestViewModel : ViewModel() {
    val state = mutableStateOf<Resource<Int>>(Resource.Success(i))

    fun deleteItem(id: Int) {
        viewModelScope.launch {
            deleteItemInternal(id).collect { response ->
                state.value = response
            }
        }
    }

    suspend fun deleteItemInternal(id: Int) = flow {
        try {
            emit(Resource.Loading)
            delay(1000)
            if (id % 3 == 0) {
                throw IllegalStateException("error on third")
            }
            emit(Resource.Success(id))
        } catch (e: Exception) {
            emit(Resource.Failure(e.message ?: e.toString()))
        }
    }
}

enter image description here

So the the problem looks like in this line itemsRef.document(id).delete().await()), or in your connection to the repository.

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • What can be the problem in this line `itemsRef.document(id).delete().await())` or in the connection to the repository? As the item is deleted successfully. – Joan P. Oct 01 '21 at 12:57
  • @JoanP. Try adding some logs to see what's going on. I'd add them on each emit line in `deleteItem` and one more inside `collect`. – Phil Dukhov Oct 01 '21 at 13:06
  • Inside the collect, I get first a print containing the address from the memory of the Loading object (@8adef02), then I get "Loading" from the layout file and second time inside collect, `Success(data=null)`. This means that I've got the data, but it's not going forward to log the `Success` :( – Joan P. Oct 01 '21 at 13:17
  • @JoanP. split `emit(Resource.Success(itemsRef.document(id).delete().await()))` in two lines, and add logs after each line – Phil Dukhov Oct 01 '21 at 13:19
  • I just did. I logged after `itemsRef.document(id).delete().await()` and after `emit(Resource.Success(deleteOperation)` and got both logs. With Success(data=null) between them. – Joan P. Oct 01 '21 at 13:25
  • @JoanP. Your `state` is private, how do you call `when(val res = viewModel.state.value)`? – Phil Dukhov Oct 01 '21 at 13:58
  • Sorry, my mistake. It's not private. I edited my question. – Joan P. Oct 01 '21 at 14:02
  • @JoanP. so according to the logs, your `state` is getting updated? Have you tried adding `Text` as I did in my example to see updates on the screen? – Phil Dukhov Oct 01 '21 at 14:05
  • Yes, the state is updated for sure. Checked that. Yes I have added for each item in the list and it is changed. That deleteItem is triggered when the corresponding item is clicked. So in this case the text is changed for each one of them :| I really appreciate your efforts. – Joan P. Oct 01 '21 at 14:10
  • @JoanP. I didn't get it, have you solved the problem? What was it? =) – Phil Dukhov Oct 01 '21 at 14:11
  • Not yet, still struggling. I'll keep trying and I'll let you know. Thank again. – Joan P. Oct 01 '21 at 14:20
  • Can it be because I'm using `viewModel: ItemsViewModel = hiltViewModel()` and not `viewModel: ItemsViewModel = viewModel()`? – Joan P. Oct 02 '21 at 12:23
  • 1
    @JoanP. It's hard to say, I don't think it should. But if you can create an example project where the problem occurs, I can take a look. – Phil Dukhov Oct 02 '21 at 15:10
0

Try collecting in the composable function:

val state = viewModel.state.collectAsState()

Then you can do: when (val res = viewModel.state.value){...}.

However I am sceptical about the deleteItem in the repository returning a flow. Do you really need such thing? You can always map stuff in the viewModel.

coroutineDispatcher
  • 7,718
  • 6
  • 30
  • 58