Mehmed
Mehmed

Reputation: 3040

Making stateful components in Android

I am using MVVM in my app. When you enter a query and click search button, the chain is as follows: Fragment -> ViewModel -> Repository -> API -> Client. The client is where HTTP requests are made. But there is one thing here, the client needs to make a call and get a key from the server at initialization. Therefore, to prevent any call before it this first call completes, I need to be able to observe it from Fragment so that I can disable search button. Since each component in the chain can communicate with adjacent components, all components should have a state.

I am thinking to implement a StatefulComponent class and make all components to extend it:

open class StatefulComponent protected constructor() {
    enum class State {
        CREATED, LOADING, LOADED, FAILED
    }

    private val currentState = MutableLiveData(State.CREATED)

    fun setState(newState: State) {
        currentState.value = newState
    }

    val state: LiveData<State> = currentState

    val isLoaded: Boolean = currentState.value == State.LOADED

    val isFailed: Boolean = currentState.value == State.FAILED

    val isCompleted: Boolean = isLoaded || isFailed
}

The idea is that each component observers the next one and updates itself accordingly. However, this is not possible for ViewModel since it is already extending ViewModel super class.

How can I implement a solution for this problem?

Upvotes: 1

Views: 511

Answers (3)

Devrath
Devrath

Reputation: 42824

If you are using the composable ... You can use produce state

@Composable
fun PokemonDetailScreen(
    viewModel: PokemonDetailVm = hiltViewModel()
) {
    /**
     * This takes a initial state and with that we get a coroutine scope where we can call a API and assign the data into the value
     */
    val pokemonInfo = produceState<Resource<Pokemon>>(initialValue = Resource.Loading()) {
        value = viewModel.getPokemonInfo(pokemonName)
    }.value

}

Upvotes: 0

Harry Luu
Harry Luu

Reputation: 521

As João Gouveia mentioned, we can make stateful components quite easily using kotlin's sealed classes.

But to make it further more useful, we can introduce Generics! So, our state class becomes StatefulData<T> which you can use pretty much anywhere (LiveData, Flows, or even in Callbacks).

sealed class StatefulData<out T : Any> {
    data class Success<T : Any>(val result : T) : StatefulData<T>()
    data class Error(val msg : String) : StatefulData<Nothing>()
    object Loading : StatefulData<Nothing>()
}

I've wrote an article fully explaining this particular implementation here https://naingaungluu.medium.com/stateful-data-on-android-with-sealed-classes-and-kotlin-flow-33e2537ccf55

Upvotes: 0

Jo&#227;o Gouveia
Jo&#227;o Gouveia

Reputation: 160

The most common approach is to use sealed class as your state, so you have any paramaters as you want on each state case.

sealed class MyState {
   object Loading : MyState()
   data class Loaded(data: Data) : MyState()
   data class Failed(message: String) : MyState()
}

On your viewmodel you will have only 1 livedata

class MyViewModel : ViewModel() {
    private val _state = MutableLiveData<MyState>()
    val state: LiveData<MyState> = _state

    fun load() {
       _state.postCall(Loading)
       repo.loadSomeData(onData = { data ->
           _state.postCall(Loaded(data))
       }, onError = { error -> _state.postCall(Failed(error.message)) })
    }

    // coroutines approach
    suspend fun loadSuspend() {
      _state.postCall(Loading)
      try {
        _state.postCall(Loaded(repo.loadSomeDataSupend()))
      } catch(e: Exception) {
        _state.postCall(Failed(e.message))
      }
    }
}

And on the fragment, just observe the state

class MyFragment : Fragment() {
   ...
   onViewCreated() {
     viewModel.state.observer(Observer {
         when (state) {
          // auto casts to each state
          Loading -> { button.isEnabled = false }
          is Loaded -> { ... }
          is Failed -> { ... }
         }
       }
     )
   }
}

Upvotes: 1

Related Questions