Dr.KeyOk
Dr.KeyOk

Reputation: 768

How to call method with StateFlow in Android

In my application I want use MVI for application architecture and for this I used StateFlow instead of LiveData.
I want when click on button call method with StateFlow.
I write below codes, after click on button show me log in viewModel but not update in Activity!
ViewModel codes :

@HiltViewModel
class MainViewModel @Inject constructor(private val repository: MainRepository) : ViewModel() {
    val mainIntent = Channel<MainIntent>()
    private val _state = MutableStateFlow<MainState>(MainState.Empty)
    val state: StateFlow<MainState> get() = _state

    init {
        handleIntent()
    }

    private fun handleIntent() {
        viewModelScope.launch {
            mainIntent.consumeAsFlow().collect {
                when (it) {
                    is MainIntent.LoadAllNotes -> fetchingAllNotesList()
                    is MainIntent.FilterNote -> fetchingFilterNotes(it.filter)
                }
            }
        }
    }

    private fun fetchingAllNotesList() {
        _state.value = MainState.LoadNotes(repository.allNotes())
    }


    private fun fetchingFilterNotes(filter: String) {
        _state.value = MainState.Empty
        Log.e("DataLog", "Empty from viewmodel")
    }
}

Activity codes :

lifecycleScope.launch {
            //Send
            viewModel.mainIntent.send(MainIntent.LoadAllNotes)
            //Get
            viewModel.state.collect { state ->
                when (state) {
                    is MainState.Empty -> {
                        Log.e("DataLog", "Empty on activity")
                        emptyLay.visibility = View.VISIBLE
                        notesList.visibility = View.GONE
                    }
                    is MainState.LoadNotes -> {
                        Log.e("DataLog", "Load notes")
                        emptyLay.visibility = View.GONE
                        notesList.visibility = View.VISIBLE
                        state.list.collect {
                            noteAdapter.setData(it)
                            notesList.apply {
                                layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
                                adapter = noteAdapter
                            }
                        }
                    }
                }
            }
        }

After click on button show me Empty from viewmodel message into logcat, but not call this method is MainState.Empty from activity!

How can I fix this problem?

Upvotes: 1

Views: 1054

Answers (1)

Tenfour04
Tenfour04

Reputation: 93541

I can't spot the error in your code, but it is more convoluted than it needs to be. Maybe if you try simplifying it, the problem will present itself more clearly or will have been resolved.

Instead of using a Channel to take commands from the Activity, the Activity can just call your functions directly. The below has the exact same functionality as you had before, but it is much simpler logic and less code.

@HiltViewModel
class MainViewModel @Inject constructor(private val repository: MainRepository) : ViewModel() {
    private val _state = MutableStateFlow<MainState>(MainState.Empty)
    val state: StateFlow<MainState> get() = _state

    fun fetchAllNotes() {
        _state.value = MainState.LoadNotes(repository.allNotes())
    }

    fun fetchFilteredNotes(filter: String) {
        _state.value = MainState.Empty
        Log.e("DataLog", "Empty from viewmodel")
    }
}

// In your activity code, change the send line:

//Send
viewModel.fetchAllNotes()

Also, I think there's another layer of complexity you can remove, but I can't be certain because I don't know the details of how your repository works. But I think it would be simpler if your state flow was just a flow of Lists of notes instead of having this MainState sealed class wrapper and a nested Flow inside that. The collect call inside another collect call may cause the first collect call to never finish processing the first item because the inner collect call never returns, so it could lock up your coroutine and cause your problem with never publishing anything to your UI.

If we assume your current MainState's list is a List<Note>, here's how I'd refactor your code to completely eliminate this extra level of complexity:

@HiltViewModel
class MainViewModel @Inject constructor(private val repository: MainRepository) : ViewModel() {
    private val fetchFlow = MutableStateFlow<Flow<List<Note>>>(emptyFlow())

    val notes: SharedFlow<List<Note>> = fetchFlow 
        .flatMapLatest { it }
        .shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    fun fetchAllNotes() {
        fetchFlow.value = repository.allNotes()
    }

    fun fetchFilteredNotes(filter: String) {
        fetchFlow.value = flowOf(emptyList()) //TODO get filtered flow using filter
        Log.e("DataLog", "Empty from viewmodel")
    }
}

Then in your activity:

lifecycleScope.launch {
    //Send
    viewModel.fetchAllNotes()
            
    //Get
    viewModel.notes.collect { list ->
        when {
            list.isEmpty() -> {
                Log.e("DataLog", "Empty on activity")
                emptyLay.visibility = View.VISIBLE
                notesList.visibility = View.GONE
            }
            else -> {
                Log.e("DataLog", "Load notes")
                emptyLay.visibility = View.GONE
                notesList.visibility = View.VISIBLE
                noteAdapter.setData(list)
                notesList.apply {
                    layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
                    adapter = noteAdapter
                }
            }
        }
    }
}

Upvotes: 2

Related Questions