Using Delegates.observable with StateFlow in Kotlin(Android) - android

I have a class which monitors the network state.
I'm using Flow to be able to collect the network state updates.
However, I also need to leave an option to use manual listeners that programmers can "hook" onto to be able to receive the network changes.
My code is simple :
private val networkTypeState = MutableStateFlow<NetworkState>(NetworkState.Unknown)
val networkTypeAsFlow: StateFlow<NetworkState> by notifyDelegate(networkTypeState)
private fun <T> notifyDelegate(init: T) =
Delegates.observable(init) { prop, _, new ->
Lg.i("notify subscribers of network update: ${prop.name} = $new")
notifySubscribers()
}
sealed class NetworkState {
object Unknown: NetworkState()
object Disconnected: NetworkState()
data class Connected(val isCellularOn: Boolean, val isWifiOn: Boolean): NetworkState()
}
Then when I update the state,
for example :
networkTypeState.value = NetworkState.Disconnected ,
the delegates.observable does not get called.
Worth noting, when I use networkTypeAsFlow.collect { .. } , this works well, meaning - the networkTypeAsFlow does get updated, it just doesn't call the delegates.observable

The Observable delegate monitors changes to the property itself. It is futile to use Observable for a val property, because a val property is never set to a new value. Mutating the object pointed at by the property is completely invisible to the property delegate.
If you want to observe changes, you can launch and collect:
private val networkTypeState = MutableStateFlow<NetworkState>(NetworkState.Unknown)
val networkTypeAsFlow: StateFlow<NetworkState> = networkTypeState
init {
viewModelScope.launch {
networkTypeAsFlow.collect {
Lg.i("notify subscribers of network update: $it")
notifySubscribers()
}
}
}
An additional benefit here is that notifySubscribers will always be called from the same dispatcher, regardless of which thread the network state was changed from.

Related

Kotlin Flow: emit loading state for every emission

My repo has the following function:
override fun getTopRatedMoviesStream(): Flow<List<Movie>>
I have the following Result wrapper:
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable? = null) : Result<Nothing>
object Loading : Result<Nothing>
}
fun <T> Flow<T>.asResult(): Flow<Result<T>> {
return this
.map<T, Result<T>> {
Result.Success(it)
}
.onStart { emit(Result.Loading) }
.catch { emit(Result.Error(it)) }
}
And finally, my ViewModel has the following UiState logic:
data class HomeUiState(
val topRatedMovies: TopRatedMoviesUiState,
val isRefreshing: Boolean
)
#Immutable
sealed interface TopRatedMoviesUiState {
data class Success(val movies: List<Movie>) : TopRatedMoviesUiState
object Error : TopRatedMoviesUiState
object Loading : TopRatedMoviesUiState
}
class HomeViewModel #Inject constructor(
private val movieRepository: MovieRepository
) : ViewModel() {
private val topRatedMovies: Flow<Result<List<Movie>>> =
movieRepository.getTopRatedMoviesStream().asResult()
private val isRefreshing = MutableStateFlow(false)
val uiState: StateFlow<HomeUiState> = combine(
topRatedMovies,
isRefreshing
) { topRatedResult, refreshing ->
val topRated: TopRatedMoviesUiState = when (topRatedResult) {
is Result.Success -> TopRatedMoviesUiState.Success(topRatedResult.data)
is Result.Loading -> TopRatedMoviesUiState.Loading
is Result.Error -> TopRatedMoviesUiState.Error
}
HomeUiState(
topRated,
refreshing
)
}
.stateIn(
scope = viewModelScope,
started = WhileUiSubscribed,
initialValue = HomeUiState(
TopRatedMoviesUiState.Loading,
isRefreshing = false
)
)
fun onRefresh() {
viewModelScope.launch(exceptionHandler) {
movieRepository.refreshTopRated()
isRefreshing.emit(true)
isRefreshing.emit(false)
}
}
The issue is the TopRatedMoviesUiState.Loading state is only emitted once on initial load but not when user pulls to refresh and new data is emitted in movieRepository.getTopRatedMoviesStream(). I understand that it is because .onStart only emits first time the Flow is subscribed to.
Do I somehow resubscribe to Flow when refresh is performed? Refresh does not always return new data from repo so how in this case, how do I avoid duplicate emission?
You emit TopRatedMoviesUiState.Loading in onStart. So what you describe is totally expected. Loading is emitted when the stream starts. I.e. when you start collecting. In your case by stateIn.
Looking at it from another perspective, how should your result wrapper know that the repository is currently loading?
And that's also the answer. Only two places in your code know that you are reloading.
Either subscribe to a fresh flow whenever you call refreshTopRated and complete the flow after emitting the final result.
Or emit a loading state right from the repository before you start loading.
I'd prefer the later one.
Neither solution will save you from emitting the same result again and again. For that, you'd need a new state like 'Unchanged' that your repository emits when it finds no new data. But please evaluate if this optimization is required. What is the cost of an extra emission?
That being said, here are some more questions, challenging your code. Hopefully guiding you to a solid implementation:
Why do you need the isRefreshing StateFlow? Is there a difference in the UI wether you initially load or wether you refresh?
Why do you need topRated to be a StateFlow? Wouldn't a regular Flow do?
Emitting two items to a StateFlow in succession (i.e. isRefreshing.emit(true); isRefreshing.emit(false) ) might loose the first emission [1]. As for state, only the most recent value is relevant. States are not events!
[1]: StateFlow has a replay buffer of 1 and a buffer overflow policy of DROP_OLDEST. See State flow is a shared flow in kotlinx-coroutines-core/kotlinx.coroutines.flow/StateFlow

Android observer is not observing

So, Im quite new in Android development and also with working on observables. So what I want to achieve is that on every scan tick of the bluetooth scanner, the scanResult gets checked and if its not in the list, the observed list scanned gets an update, which checks something if this is valid and makes something.
My Problem is that the observe function just runs through once and then never again
2021-03-05 22:18:54.522 32057-32057/? D/devices: Got something []
2021-03-05 22:18:54.522 32057-32057/? D/devices: Start scan
2021-03-05 22:18:55.209 32057-32057/? D/devices: Checking Had128_1_1 in scanned
2021-03-05 22:18:55.212 32057-32057/? D/devices: adding Had128_1_1 in scanned
As far as I understood it right how this should work, he should here _scanned.value?.add(scanResult.device) update and the observer should recognize that or am I wrong?
Could id be a problem with the lifecycleOwner?
It comes from the MainActivity to the composable with val lifecycleOwner = LocalLifecycleOwner.current
And yes, the function gets called in my composable (as I said, if i press the button, I can see on "Got Something []" that it runs through the first time)
private val _scanned: MutableLiveData<MutableList<BluetoothDevice>> =
MutableLiveData(mutableListOf())
private val scanned: LiveData<MutableList<BluetoothDevice>> get() = _scanned
private val lockScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
result?.let { scanResult ->
if (fakeWhitelist.contains(scanResult.device.name)) {
Log.d("devices", "Checking ${scanResult.device.name} in scanned")
val search =
_scanned.value?.find { device -> device.name == scanResult.device.name }
if (search == null) {
Log.d("devices", "adding ${scanResult.device.name} in scanned")
_scanned.value?.add(scanResult.device)
}
}
}
}
}
fun listenForDevices(viewLiveCycleOwner: LifecycleOwner) {
scanned.observe(viewLiveCycleOwner, Observer { deviceList ->
Log.d("devices", "Got something $deviceList")
locationList.value.forEach { locationScaffold ->
val boxNames = locationScaffold.parcelBox.map { it.boxName }
val device = deviceList.last()
Log.d("devices", "$boxNames")
if (!boxNames.contains(device.name)) {
Log.d("devices", "Getting location for ${device.name}")
getLocation(device.name)
}
}
})
startScan()
}
Your mistake here is that you are never changing the value of the LiveData. The value of LiveData can be retrieved using LiveData.getValue() and can only be set using LiveData.setValue() and if you have a look over your implementation, you don't use the set method anywhere.
It is quite a common misconception that changing the state of an object will do something to it's reference, or somehow notify a class holding a reference to it, but this is not the case. If you think about a simplified version of LiveData you can break it down into having these core functions:
Having a current value
Holding a list of potential observers
Notifying said observers when the value is changed
The only way for it to be able to notify the observers is to expose a method/function that not only sets the value, but also notifies all the observers of the change if necessary.
So in your implementation, you retrieved the current value and changed its state, but never actually set a new value to the LiveData.
So instead of
liveData.value?.add(newItem)
We would need to do something like
val list = liveData.value
list?.add(newItem)
_mutableLiveData.value = list

android coroutines flow manual retry

I'm using stateFlow in my viewModel to get the result of api calls with a sealed class like this :
sealed class Resource<out T> {
data class Loading<T>(val data: T?): Resource<T>()
data class Success<T>(val data: T?): Resource<T>()
data class Error<T>(val error: Exception, val data: T?, val time: Long = System.currentTimeMillis()): Resource<Nothing>()
}
class VehicleViewModel #ViewModelInject constructor(application: Application, private val vehicleRepository: VehicleRepository): BaseViewModel(application) {
val vehiclesResource: StateFlow<Resource<List<Vehicle>>> = vehicleRepository.getVehicles().shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
}
I would like to propose in my UI a button so that if the api call fails, the user can retry the call. I know there is a retry method on flows but it can not be called manually as it is only triggered when an exception occurs.
A common use case would be: The user have no internet connection, when the api call returns a network exception, I show a message to the user telling him to check its connection, then with a retry button (or by detecting that the device is now connected, whatever), I retry the flow.
But I can't figure a way to do it as you can not, like call flow.retry(). The actual retry method will be called as soon as an exception occurs. I don't want to retry immediately without asking the user to check it's connection, it wouldn't make sense.
Actually the only solution I found is to recreate the activity when the retry button is pressed so the flow will be reseted, but it's terrible for performances of course.
Without flows the solution is simple and there is plenties of examples , you just have to relaunch the job, but I can't find a way to do it properly with flows. I have logic in my repository between the local room database and the remote service and the flow api is really nice so I would like to use it for this use case too.
I recommend keep the vehicleResource as a field in viewmodel and call a function to make an API call to fetch data.
private val _vehiclesResource = MutableStateFlow<Resource<List<Vehicle>>>(Resource.Loading(emptyList()))
val vehiclesResource: StateFlow<Resource<List<Vehicle>>> = _vehiclesResource.asStateFlow()
init {
fetchVehicles()
}
private var vehicleJob : Job? = null
fun fetchVehicles() {
vehicleJob?.cancel()
vehicleJob = viewModelScope.launch {
vehicleRepository.getVehicles().collect {
_vehiclesResource.value = it
}
}
}
The API will be called in the constructor of viewmodel. And also you can call this function via the view (button's click listener) for error state.
P.S: I should mention that this line of your code has issue and SharedFlow couldn't be cast to StateFlow.
val vehiclesResource: StateFlow<Resource<List<Vehicle>>> = vehicleRepository.getVehicles().shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)

Use LiveData without Lifecycle Owner

I could not find any information, if it's a bad idea to use LiveData without a lifecycle owner. And if it is, what could be the alternative?
Let me give you just a simple example
class Item() {
private lateinit var property: MutableLiveData<Boolean>
init {
property.value = false
}
fun getProperty(): LiveData<Boolean> = property
fun toggleProperty() {
property.value = when (property.value) {
false -> true
else -> false
}
}
}
class ItemHolder {
private val item = Item()
private lateinit var observer: Observer<Boolean>
fun init() {
observer = Observer<Boolean> { item ->
updateView(item)
}
item.getProperty().observeForever(observer)
}
fun destroy() {
item.getProperty().removeObserver(observer)
}
fun clickOnButton() {
item.toggleProperty();
}
private fun updateView(item: Boolean?) {
// do something
}
}
You can register an observer without an associated LifecycleOwner object using the
observeForever(Observer) method
like that:
orderRepo.getServices().observeForever(new Observer<List<Order>>() {
#Override
public void onChanged(List<Order> orders) {
//
}
});
You can register an observer without an associated LifecycleOwner object using the observeForever(Observer) method. In this case, the observer is considered to be always active and is therefore always notified about modifications. You can remove these observers calling the removeObserver(Observer) method.
Ref
https://developer.android.com/topic/libraries/architecture/livedata.html#work_livedata
For me LiveData has two benefits:
It aware of life cycle events and will deliver updates only in an appropriate state of a subscriber (Activity/Fragment).
It holds the last posted value, and updates with it new subscribers.
As already been said, if you're using it out of the life cycle components (Activity/Fragment) and the delivered update could be managed anytime, then you can use it without life cycle holder, otherwise, sooner or later, it may result in a crash, or data loss.
As an alternative to the LiveData behavior, I can suggest a BehaviourSubject from RxJava2 framework, which acts almost the same, holding the last updated value, and updating with it new subscribers.

Persist viewstate of a dialog on orientation change

In my current Android project I have a dialog that retrieves a list of objects from a webservice and show these in a list. It has a problem though. The webservice (outside my control) is not the fastest, so the process takes a while and often the user changes the orientation of the device while this process is ongoing. For now the orientation change results in the original webservice call to be cancelled and a new one is created, but this is not how it should be done I know. I would like the dialog to able to continue the loading from the webservice when an orientation change happens, but I just can't get my head around how to do this. How is it possible to hook into an ongoing call and display this state in the view (the dialog)? Any recommendations are appreciated. I've played around with Android Architecture Components and kotlins sealed classes saving the viewstate in a livedata object that the view observes, but have not found a solution that I like.
I believe a lot of developers use RxJava for this kind of issue. Is this my only option?
Btw. at the moment I use MVP architecture in my project.
Edit
This is where I cancel the job - if the listener is null its cancelled (I'm using kotlin coroutines):
override fun getWorklist() {
job = onWorkerThread {
val result = repository.getResult().awaitResult()
onMainThread {
when (result) {
is Result.Ok -> listener?.onResult(result.getOrDefault(emptyList())) :? job.cancel()
// Any HTTP error
is Result.Error -> listener?.onHttpError(result.exception) :? job.cancel()
// Exception while request invocation
is Result.Exception -> listener?.onException(result.exception) :? job.cancel()
}
}
}
}
Edit 2
I've tried controlling the viewstate using this Android Arch ViewModel, but the MediatorLiveData object isn't triggered on changes of the viewstate object. And the view stays in Loading state:
class MainModel : ViewModel() {
private var job: Job? = null
val viewState = MutableLiveData<MainViewState>().apply { value = Loading("Warsaw") }
val dataGetter = MediatorLiveData<MainViewState>().apply {
addSource(viewState, {
Log.d("MainModel", "Viewstate is: " + viewState.value.toString() + ")...")
when(it) {
is Loading -> {
Log.d("MainModel", "Launching coroutine...")
job = launch(UI) {
Log.d("MainModel", "Loading...")
val items = Repository.getWorklist()
Log.d("MainModel", "Charts retrieved...")
this#MainModel.viewState.postValue(Success(it.items))
Log.d("MainModel", "Posted update to viewstate...")
}
}
}
})
}
override fun onCleared() {
Log.d("MainModel", "Clearing ViewModel")
job?.cancel()
}
}
interface ViewState
sealed class MainViewState : ViewState
class Loading() : MainViewState()
class Error(val error: String) : MainViewState()
class Success(val items: List<WorklistItem>) : MainViewState()

Categories

Resources