I have a huge project and I need to refactor code to LiveData (not Flow). I have an Order and states in ViewModel. I cannot receive this Order in Activity when I observe it. How can I do this? This is my View Model:
private var _basicModel: MutableLiveData<OrderUiState> = MutableLiveData()
val basicModel: LiveData<OrderUiState> get() = _basicModel
sealed class OrderUiState {
object Loading : OrderUiState()
data class OrderFail(val message: String) : OrderUiState()
data class OrderSuccess(val order: Order) : OrderUiState()
}
fun getOrder(orderId: String) {
viewModelScope.launch {
_basicModel.value = OrderUiState.Loading
getOrderUseCase.execute(orderId, { order ->
_basicModel.value = OrderUiState.OrderSuccess(order)
}
}
And now I cannot to get to Order, when I have Succes Sate. My code want from me in Activity order, but I thought, that whan it is success, there it will be, but isn't?
viewModel.basicModel.observe(this) { order ->
when(order){
OrderViewModel.OrderUiState.OrderSuccess(here he want from me order... )
}
}
Can I get to order from this code?
You can do it this way:
viewModel.basicModel.observe(this) { uiState ->
when(uiState) {
is OrderViewModel.OrderUiState.OrderSuccess -> {
val order = uiState.order
// Use the order here
}
}
}
Related
I have a common situation of getting data. I use the Kotlin Coroutines.
1 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
lateinit var data: List<String>
init {
viewModelScope.launch {
data = gettingData.get()
}
}
}
2 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = MutableStateFlow<List<String>?>(null)
init {
viewModelScope.launch {
data.emit(gettingData.get())
}
}
}
How can I initialize a data field not delayed, but immediately, with the viewModelScope but without a lateinit or nullble field? And without LiveData, my progect uses Coroutine Flow
I can't return a result of viewModelScope job in .run{} or by lazy {}.
I cant return a result drom fun:
val data: List<String> = getData()
fun getData(): List<String> {
viewModelScope.launch {
data = gettingData.get()
}
return ???
}
Also I can't make suspend fun getData() because I can't create coroutineScope in initialisation'
You're describing an impossibility. Presumably, gettingData.get() is defined as a suspend function, meaning the result literally cannot be retrieved immediately. Since it takes a while to retrieve, you cannot have an immediate value.
This is why apps and websites have loading indicators in their UI.
If you're using Flows, you can use a Flow with a nullable type (like in your option 2 above), and in your Activity/Fragment, in the collector, you show either a loading indicator or your data depending on whether it is null.
Your code 2 can be simplified using the flow builder and stateIn with a null default value:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = flow<List<String>?> { emit(gettingData.get()) }
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
}
In your Activity or Fragment:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.data
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { list ->
if(list == null) {
// Show loading indicator in UI
} else {
// Show the data
}
}
}
If your data loads pretty quickly, instead of making the type nullable, you can just make the default value emptyList(). Then your collector can just not do anything when the list is empty. This works if the data loads quickly enough that the user isn't going to wonder if something is wrong because the screen is blank for so long.
You have to use SharedFlow with replay 1 (to store last value and replay it for a new subscriber) to implement it.
My sample:
interface DataSource {
suspend fun getData(): Int
}
class DataViewModel(dataSource: DataSource): ViewModel() {
val dataField =
flow<Int> {
emit(dataSource.getData())
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(1000), 1)
}
I'm building a KMM app for retrieving news.
My app fetches news every 30 seconds and save it in a local database. User must be logged for use it. When user want to logout i need to stop refreshing news and delete the local database.
How do i stop a flow with an infinite loop properly without use static variabile?
I designed the app like follows:
ViewModel (separate for Android and iOS)
UseCase (shared)
Repository (shared)
Data source (shared)
Android Jetpack compose single activity
iOS SwiftUI
Android ViewModel:(iOS use ObservableObject, but logic is the same)
#HiltViewModel
class NewsViewModel #Inject constructor(
private val startFetchingNews: GetNewsUseCase,
private val stopFetchingNews: StopGettingNewsUseCase,
) : ViewModel() {
private val _mutableNewsUiState = MutableStateFlow(NewsState())
val newsUiState: StateFlow<NewsState> get() = _mutableNewsUiState.asStateFlow()
fun onTriggerEvent(action: MapEvents) {
when (action) {
is NewsEvent.GetNews -> {
getNews()
}
is MapEvents.StopNews -> {
//????
}
else -> {
}
}
}
private fun getNews()() {
startFetchingNews().collectCommon(viewModelScope) { result ->
when {
result.error -> {
//update state
}
result.succeeded -> {
//update state
}
}
}
}
}
UseCase:
class GetNewsUseCase(
private val newsRepo: NewsRepoInterface) {
companion object {
private val UPDATE_INTERVAL = 30.seconds
}
operator fun invoke(): CommonFlow<Result<List<News>>> = flow {
while (true) {
emit(Result.loading())
val result = newsRepo.getNews()
if (result.succeeded) {
// emit result
} else {
//emit error
}
delay(UPDATE_INTERVAL)
}
}.asCommonFlow()
}
Repository:
class NewsRepository(
private val sourceNews: SourceNews,
private val cacheNews: CacheNews) : NewsRepoInterface {
override suspend fun getNews(): Result<List<News>> {
val news = sourceNews.fetchNews()
//.....
cacheNews.insert(news) //could be a lot of news
return Result.data(cacheNews.selectAll())
}
}
Flow extension functions:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun collectCommon(
coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
callback: (T) -> Unit, // callback on each emission
) {
onEach {
callback(it)
}.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
}
I tried to move the while loop inside repository, so maybe i can break the loop with a singleton repository, but then i must change the getNews method to flow and collect inside GetNewsUseCase (so a flow inside another flow).
Thanks for helping!
When you call launchIn on a Flow, it returns a Job. Hang on to a reference to this Job in a property, and you can call cancel() on it when you want to stop collecting it.
I don't see the point of the CommonFlow class. You could simply write collectCommon as an extension function of Flow directly.
fun <T> Flow<T>.collectCommon(
coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
callback: (T) -> Unit, // callback on each emission
): Job {
return onEach {
callback(it)
}.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
// ...
private var fetchNewsJob: Job? = null
private fun getNews()() {
fetchNewsJob = startFetchingNews().collectCommon(viewModelScope) { result ->
when {
result.error -> {
//update state
}
result.succeeded -> {
//update state
}
}
}
}
In my opinion, collectCommon should be eliminated entirely because all it does is obfuscate your code a little bit. It saves only one line of code at the expense of clarity. It's kind of an antipattern to create a CoroutineScope whose reference you do not keep so you can manage the coroutines running in it--might as well use GlobalScope instead to be clear you don't intend to manage the scope lifecycle so it becomes clear you must manually cancel the Job, not just in the case of the news source change, but also when the UI it's associated with goes out of scope.
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?
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 -> { ... }
}
}
)
}
}
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
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
}
Background
I'm trying to implement a MVVM-style clean architecture pattern with repositories and usecases/interactors. I would like to use Kotlin Flows for the usecases/interactors. All of the usecases have the same setup and the result is wrapped in a sealed class.
Response wrapper:
sealed class Response<out T> {
object Loading : Response<Nothing>()
data class Success<T>(val data: T? = null) : Response<T>()
data class Error(val error: ErrorEntity? = null) : Response<Nothing>()
data class Empty(val msg: Int = R.string.empty_string) : Response<Nothing>()
}
all UseCases/Interactors implement:
interface UseCase<T, Params> {
fun execute(params: Params? = null) : Flow<Response<T>>
}
Problem
In my example I need to use the result of a class GetFbUserUseCase inside the result of GetAllUsersUseCase. Both of them emit a Loading, Error and Result state which i would like to delegate to the UI directly.
Example Code
class TaskEditViewModel(
private val getCurrentFbUserUseCase: GetFbUserUseCase,
private val getAllUsersUseCase: GetAllUsersUseCase
) : ViewModel() {
private val _pageState = MutableLiveData<Response<*>>()
val pageState: LiveData<Response<*>>
get() = _pageState
fun getUsers() {
viewModelScope.launch {
// get current user ID from GetFbUserUseCase.
val firebaseUser: Flow<Response<FirebaseUser?>> = getCurrentFbUserUseCase.execute()
// get all users from GetAllUsersUseCase.
val userList: Flow<Response<List<User>>> = getAllUsersUseCase.execute()
// somehow combine both results??
merge(firebaseUser, userList).collect { response ->
// delegate the combined Loading, Error states to the UI ?
_pageState.value = response
// only handle the Success state in the viewmodel?
when (response) {
is Response.Success<*> -> {
// get current user ID from GetFbUserUseCase
// apply filtering on the result of `GetAllUsersUseCase` with the result
// from `GetFbUserUseCase` and show different UI accordingly
if (response.data.filterNot { it.userId == currentUser.userId }.isEmpty()) {
// notify liveData to show current user
} else {
// notify liveData to show complete user list
}
}
}
}
}
}
Question:
according to: Kotlin flows,
There are multiple options to compose and flatten multiple flows. Which one would best suit my Problem and how would I implement this?
I see combine solves your problem.
Here is how to combine these two flows
firebaseUser.combine(userList).collect { fbuser, userlist ->
//combine the results and set the livedata here
someFunThatSetsLiveData(fbuser, userlist)
}
Whenever one of these flows emit new result, the someFunThatSetsLiveData will be called.
I'm using LiveData's version "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha05". Once my LiveData block executes successfully I want to explicitly trigger it to execute again, e.g.
I navigate to a fragment
User's data loads
I click delete btn while being in the same fragment
User's data should refresh
I have a fragment where I observe my LiveData, a ViewModel with LiveData and Repository:
ViewModel:
fun getUserLiveData() = liveData(Dispatchers.IO) {
val userData = usersRepo.getUser(userId)
emit(userData)
}
Fragment:
viewModel.getUserLiveData.observe(viewLifecycleOwner,
androidx.lifecycle.Observer {..
Then I'm trying to achieve desired behaviour like this:
viewModel.deleteUser()
viewModel.getUserLiveData()
According to the documentation below LiveData block won't execute if it has completed successfully and if I put a while(true) inside the LiveData block, then my data refreshes, however I don't want this to do since I need to update my view reactively.
If the [block] completes successfully or is cancelled due to reasons other than [LiveData]
becoming inactive, it will not be re-executed even after [LiveData] goes through active
inactive cycle.
Perhaps I'm missing something how I can reuse the same LiveDataScope to achieve this? Any help would be appreciated.
To do this with liveData { .. } block you need to define some source of commands and then subscribe to them in a block. Example:
MyViewModel() : ViewModel() {
val commandsChannel = Channel<Command>()
val liveData = livedata {
commandsChannel.consumeEach { command ->
// you could have different kind of commands
//or emit just Unit to notify, that refresh is needed
val newData = getSomeNewData()
emit(newData)
}
}
fun deleteUser() {
.... // delete user
commandsChannel.send(RefreshUsersListCommand)
}
}
Question you should ask yourself: Maybe it would be easier to use ordinary MutableLiveData instead, and mutate its value by yourself?
livedata { ... } builder works well, when you can collect some stream of data (like a Flow / Flowable from Room DB) and not so well for plain, non stream sources, which you need to ask for data by yourself.
I found a solution for this. We can use switchMap to call the LiveDataScope manually.
First, let see the official example for switchMap:
/**
* Here is an example class that holds a typed-in name of a user
* `String` (such as from an `EditText`) in a [MutableLiveData] and
* returns a `LiveData` containing a List of `User` objects for users that have
* that name. It populates that `LiveData` by requerying a repository-pattern object
* each time the typed name changes.
* <p>
* This `ViewModel` would permit the observing UI to update "live" as the user ID text
* changes.
**/
class UserViewModel: AndroidViewModel {
val nameQueryLiveData : MutableLiveData<String> = ...
fun usersWithNameLiveData(): LiveData<List<String>> = nameQueryLiveData.switchMap {
name -> myDataSource.usersWithNameLiveData(name)
}
fun setNameQuery(val name: String) {
this.nameQueryLiveData.value = name;
}
}
The example was very clear. We just need to change nameQueryLiveData to your own type and then combine it with LiveDataScope. Such as:
class UserViewModel: AndroidViewModel {
val _action : MutableLiveData<NetworkAction> = ...
fun usersWithNameLiveData(): LiveData<List<String>> = _action.switchMap {
action -> liveData(Dispatchers.IO){
when (action) {
Init -> {
// first network request or fragment reusing
// check cache or something you saved.
val cache = getCache()
if (cache == null) {
// real fecth data from network
cache = repo.loadData()
}
saveCache(cache)
emit(cache)
}
Reload -> {
val ret = repo.loadData()
saveCache(ret)
emit(ret)
}
}
}
}
// call this in activity, fragment or any view
fun fetchData(ac: NetworkAction) {
this._action.value = ac;
}
sealed class NetworkAction{
object Init:NetworkAction()
object Reload:NetworkAction()
}
}
First add implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" to your gradle file. Make your ViewModel as follows:
MyViewModel() : ViewModel() {
val userList = MutableLiveData<MutableList<User>>()
fun getUserList() {
viewModelScope.launch {
userList.postValue(usersRepo.getUser(userId))
}
}
}
Then onserve the userList:
viewModel.sessionChartData.observe(viewLifecycleOwner, Observer { users ->
// Do whatever you want with "users" data
})
Make an extension to delete single user from userList and get notified:
fun <T> MutableLiveData<MutableList<T>>.removeItemAt(index: Int) {
if (!this.value.isNullOrEmpty()) {
val oldValue = this.value
oldValue?.removeAt(index)
this.value = oldValue
} else {
this.value = mutableListOf()
}
}
Call that extension function to delete any user and you will be notified in your Observer block after one user get deleted.
viewModel.userList.removeItemAt(5) // Index 5
When you want to get userList from data source just call viewModel.getUserList() You will get data to the observer block.
private val usersLiveData = liveData(Dispatchers.IO) {
val retrievedUsers = MyApplication.moodle.getEnrolledUsersCoroutine(course)
repo.users = retrievedUsers
roles.postValue(repo.findRolesByAll())
emit(retrievedUsers)
}
init {
usersMediator.addSource(usersLiveData){ usersMediator.value = it }
}
fun refreshUsers() {
usersMediator.removeSource(usersLiveData)
usersMediator.addSource(usersLiveData) { usersMediator.value = it }
The commands in liveData block {} doesn't get executed again.
Okay yes, the observer in the viewmodel holding activity get's triggered, but with old data.
No further network call.
Sad. Very sad. "Solution" seemed promisingly and less boilerplaty compared to the other suggestions with Channel and SwitchMap mechanisms.
You can use MediatorLiveData for this.
The following is a gist of how you may be able to achieve this.
class YourViewModel : ViewModel() {
val mediatorLiveData = MediatorLiveData<String>()
private val liveData = liveData<String> { }
init {
mediatorLiveData.addSource(liveData){mediatorLiveData.value = it}
}
fun refresh() {
mediatorLiveData.removeSource(liveData)
mediatorLiveData.addSource(liveData) {mediatorLiveData.value = it}
}
}
Expose mediatorLiveData to your View and observe() the same, call refresh() when your user is deleted and the rest should work as is.