I have a problem with getting the last emitted value from the Subject
This is my class which is responsible for emitting and observing the battery changes:
class BatteryLevelProvider #Inject constructor(
app: App
) {
private val context: Context = app
private val receiver: PowerConnectionReceiver = PowerConnectionReceiver()
init {
initializeReceiver()
}
private fun initializeReceiver() {
IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { intentFilter ->
context.registerReceiver(receiver, intentFilter)
}
}
companion object {
val batteryLevelSubject = PublishSubject.create<Int>()
}
fun observeBatteryLevel(): Observable<Int> = batteryLevelSubject.distinctUntilChanged()
fun getCurrentBatteryLevel(): Int {
Timber.d("getCurrentBatteryLevel: ENTERED")
val blockingLast = batteryLevelSubject.blockingLast(0)
Timber.d("getCurrentBatteryLevel: $blockingLast")
return blockingLast
}
inner class PowerConnectionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
val percentage= (level / scale.toFloat() * 100).toInt()
batteryLevelSubject.onNext(percentage)
Timber.d("Battery changed: $percentage")
}
}
}
When i invoke the
getCurrentBatteryLevel()
It reach the blockingLast never get return the value and hangs the app.
What is the reason and how to handle this properly?
subject.blockingLast(0) means the following: get the last value after the stream has completed emitting values and if it has completed without emitting anything then return the default value.
That means that blockingLast will wait until it receives onComplete event because only then it can figure out that the stream has ended (and emit last value). PublishSubject creates an infinite stream and you never call batteryLevelSubject.onComplete to end the stream manually and that's why it hangs forever.
You can easily fix that by changing PublishSubject to BehaviorSubject. The main difference between them is that BehaviorSubject caches the last received value which can then be received by anyone. Also, you need to change batteryLevelSubject.blockingLast(0) to batteryLevelSubject.value to get the last cached value (and it won't block anything!). But be aware that the value may be null at the first run when you haven't put there anything yet. You can easily fix that by creating BehaviorSubject with default value like so:
val subject = BehaviorSubject.createDefault(0)
Related
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.
I saw this but I'm not sure how to implement it or if this is the same issue, I have a mediator live data that updates when either of its 2 source live datas update or when the underlying data (Room db) updates, it seems to work fine but if the data updates a lot it refreshes a lot in quick succession and I get an error
Cannot run invalidation tracker. Is the db closed?
Cannot access database on the main thread since it may potentially lock the UI for a long period of time
this doesn't happen everytime, only when there are a lot of updates to the database in very quick succession heres the problem part of the view model,
var search: MutableLiveData<String> = getSearchState()
val filters: MutableLiveData<MutableSet<String>> = getCurrentFiltersState()
val searchPokemon: LiveData<PagingData<PokemonWithTypesAndSpeciesForList>>
val isFiltersLayoutExpanded: MutableLiveData<Boolean> = getFiltersLayoutExpanded()
init {
val combinedValues =
MediatorLiveData<Pair<String?, MutableSet<String>?>?>().apply {
addSource(search) {
value = Pair(it, filters.value)
}
addSource(filters) {
value = Pair(search.value, it)
}
}
searchPokemon = Transformations.switchMap(combinedValues) { pair ->
val search = pair?.first
val filters = pair?.second
if (search != null && filters != null) {
searchAndFilterPokemonPager(search, filters.toList())
} else null
}.distinctUntilChanged()
}
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonPager(search: String, filters: List<String>): LiveData<PagingData<PokemonWithTypesAndSpeciesForList>> {
return Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
maxSize = 60
)
) {
if (filters.isEmpty()){
searchPokemonForPaging(search)
} else {
searchAndFilterPokemonForPaging(search, filters)
}
}.liveData.cachedIn(viewModelScope)
}
#SuppressLint("DefaultLocale")
private fun getAllPokemonForPaging(): PagingSource<Int, PokemonWithTypesAndSpecies> {
return repository.getAllPokemonWithTypesAndSpeciesForPaging()
}
#SuppressLint("DefaultLocale")
private fun searchPokemonForPaging(search: String): PagingSource<Int, PokemonWithTypesAndSpeciesForList> {
return repository.searchPokemonWithTypesAndSpeciesForPaging(search)
}
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonForPaging(search: String, filters: List<String>): PagingSource<Int, PokemonWithTypesAndSpeciesForList> {
return repository.searchAndFilterPokemonWithTypesAndSpeciesForPaging(search, filters)
}
the error is actually thrown from the function searchPokemonForPaging
for instance it happens when the app starts which does about 300 writes but if I force the calls off the main thread by making everything suspend and use runBlocking to return the Pager it does work and I don't get the error anymore but it obviously blocks the ui, so is there a way to maybe make the switchmap asynchronous or make the searchAndFilterPokemonPager method return a pager asynchronously? i know the second is technically possible (return from async) but maybe there is a way for coroutines to solve this
thanks for any help
You can simplify combining using combineTuple (which is available as a library that I wrote for this specific purpose) (optional)
Afterwards, you can use the liveData { coroutine builder to move execution to background thread
Now your code will look like
val search: MutableLiveData<String> = getSearchState()
val filters: MutableLiveData<Set<String>> = getCurrentFiltersState()
val searchPokemon: LiveData<PagingData<PokemonWithTypesAndSpeciesForList>>
val isFiltersLayoutExpanded: MutableLiveData<Boolean> = getFiltersLayoutExpanded()
init {
searchPokemon = combineTuple(search, filters).switchMap { (search, filters) ->
liveData {
val search = search ?: return#liveData
val filters = filters ?: return#liveData
withContext(Dispatchers.IO) {
emit(searchAndFilterPokemonPager(search, filters.toList()))
}
}
}.distinctUntilChanged()
}
I'm using WorkManager for deferred work in my app.
The total work is divided into a number of chained workers, and I'm having trouble showing the workers' progress to the user (using progress bar).
I tried creating one tag and add it to the different workers, and inside the workers update the progress by that tag, but when I debug I always get progress is '0'.
Another thing I noticed is that the workManager's list of work infos is getting bigger each time I start the work (even if the workers finished their work).
Here is my code:
//inside view model
private val workManager = WorkManager.getInstance(appContext)
internal val progressWorkInfoItems: LiveData<List<WorkInfo>>
init
{
progressWorkInfoItems = workManager.getWorkInfosByTagLiveData(TAG_SAVING_PROGRESS)
}
companion object
{
const val TAG_SAVING_PROGRESS = "saving_progress_tag"
}
//inside a method
var workContinuation = workManager.beginWith(OneTimeWorkRequest.from(firstWorker::class.java))
val secondWorkRequest = OneTimeWorkRequestBuilder<SecondWorker>()
secondWorkRequest.addTag(TAG_SAVING_PROGRESS)
secondWorkRequest.setInputData(createData())
workContinuation = workContinuation.then(secondWorkRequest.build())
val thirdWorkRequest = OneTimeWorkRequestBuilder<ThirdWorker>()
thirdWorkRequest.addTag(TAG_SAVING_PROGRESS)
thirdWorkRequest.setInputData(createData())
workContinuation = workContinuation.then(thirdWorkRequest.build())
workContinuation.enqueue()
//inside the Activity
viewModel.progressWorkInfoItems.observe(this, observeProgress())
private fun observeProgress(): Observer<List<WorkInfo>>
{
return Observer { listOfWorkInfo ->
if (listOfWorkInfo.isNullOrEmpty()) { return#Observer }
listOfWorkInfo.forEach { workInfo ->
if (WorkInfo.State.RUNNING == workInfo.state)
{
val progress = workInfo.progress.getFloat(TAG_SAVING_PROGRESS, 0f)
progress_bar?.progress = progress
}
}
}
}
//inside the worker
override suspend fun doWork(): Result = withContext(Dispatchers.IO)
{
setProgress(workDataOf(TAG_SAVING_PROGRESS to 10f))
...
...
Result.success()
}
The setProgress method is to observe intermediate progress in a single Worker (as explained in the guide):
Progress information can only be observed and updated while the ListenableWorker is running.
For this reason, the progress information is available only till a Worker is active (e.g. it is not in a terminal state like SUCCEEDED, FAILED and CANCELLED). This WorkManager guide covers Worker's states.
My suggestion is to use the Worker's unique ID to identify which worker in your chain is not yet in a terminal state. You can use WorkRequest's getId method to retrieve its unique ID.
According to my analysis I have found that there might be two reasons why you always get 0
setProgress is set just before the Result.success() in the doWork() of the worker then it's lost and you never get that value in your listener. This could be because the state of the worker is now SUCCEEDED
the worker is completing its work in fraction of seconds
Lets take a look at the following code
class Worker1(context: Context, workerParameters: WorkerParameters) : Worker(context,workerParameters) {
override fun doWork(): Result {
setProgressAsync(Data.Builder().putInt("progress",10).build())
for (i in 1..5) {
SystemClock.sleep(1000)
}
setProgressAsync(Data.Builder().putInt("progress",50).build())
SystemClock.sleep(1000)
return Result.success()
}
}
In the above code
if you remove only the first sleep method then the listener only get the progres50
if you remove only the second sleep method then the listener only get the progress 10
If you remove both then the you get the default value 0
This analysis is based on the WorkManager version 2.4.0
Hence I found that the following way is better and always reliable to show the progress of various workers of your chain work.
I have two workers that needs to be run one after the other. If the first work is completed then 50% of the work is done and 100% would be done when the second work is completed.
Two workers
class Worker1(context: Context, workerParameters: WorkerParameters) : Worker(context,workerParameters) {
override fun doWork(): Result {
for (i in 1..5) {
Log.e("worker", "worker1----$i")
}
return Result.success(Data.Builder().putInt("progress",50).build())
}
}
class Worker2(context: Context, workerParameters: WorkerParameters) : Worker(context,workerParameters) {
override fun doWork(): Result {
for (i in 5..10) {
Log.e("worker", "worker1----$i")
}
return Result.success(Data.Builder().putInt("progress",100).build())
}
}
Inside the activity
workManager = WorkManager.getInstance(this)
workRequest1 = OneTimeWorkRequest.Builder(Worker1::class.java)
.addTag(TAG_SAVING_PROGRESS)
.build()
workRequest2 = OneTimeWorkRequest.Builder(Worker2::class.java)
.addTag(TAG_SAVING_PROGRESS)
.build()
findViewById<Button>(R.id.btn).setOnClickListener(View.OnClickListener { view ->
workManager?.
beginUniqueWork(TAG_SAVING_PROGRESS,ExistingWorkPolicy.REPLACE,workRequest1)
?.then(workRequest2)
?.enqueue()
})
progressBar = findViewById(R.id.progressBar)
workManager?.getWorkInfoByIdLiveData(workRequest1.id)
?.observe(this, Observer { workInfo: WorkInfo? ->
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
val progress = workInfo.outputData
val value = progress.getInt("progress", 0)
progressBar?.progress = value
}
})
workManager?.getWorkInfoByIdLiveData(workRequest2.id)
?.observe(this, Observer { workInfo: WorkInfo? ->
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
val progress = workInfo.outputData
val value = progress.getInt("progress", 0)
progressBar?.progress = value
}
})
The reason workManager's list of work infos is getting bigger each time the work is started even if the workers finished their work is because of
workManager.beginWith(OneTimeWorkRequest.from(firstWorker::class.java))
instead one need to use
workManager?.beginUniqueWork(TAG_SAVING_PROGRESS, ExistingWorkPolicy.REPLACE,OneTimeWorkRequest.from(firstWorker::class.java))
You can read more about it here
I am using Room database with LiveData. In Main activity I am showing data for current day. But when new day comes and onCreate wasn't called, views shows data for previous day. How can I properly refresh my data/views in onResume?
MainActivity:
mTodayViewModel = ViewModelProviders.of(this).get(TodayDataViewModel::class.java)
val todayDataObserver = Observer<CoffeeProductivityData> { todayData ->
... update views here }
mTodayViewModel.getTodayData().observe(this, todayDataObserver)
ViewModel:
class TodayDataViewModel(application: Application) : AndroidViewModel(application) {
private val mRepository: CoffeeProductivityRepository = CoffeeProductivityRepository(application)
private val mTodayData: LiveData<CoffeeProductivityData> by lazy {
mRepository.getTodayData()
}
fun getTodayData(): LiveData<CoffeeProductivityData> {
return mTodayData
}}
Repository:
private var mCoffeeProductivityDao: CoffeeProductivityDao
private var mTodayData: LiveData<CoffeeProductivityData>
private var mUtilities: Utilities
init {
val database: CoffeeProductivityDatabase = CoffeeProductivityDatabase.getDatabase(application)!!
mCoffeeProductivityDao = database.coffeeProductivityDao()
mUtilities = Utilities()
mTodayData = mCoffeeProductivityDao.getTodayData(mUtilities.getTodayDate())
}
// Wrapper for getting current day data
fun getTodayData(): LiveData<CoffeeProductivityData> {
return mTodayData
}
Query from DAO:
#Query("SELECT * from coffee_productivity WHERE date LIKE :todayDate")
fun getTodayData(todayDate: String): LiveData<CoffeeProductivityData>
I think your best option is to listen to the ACTION_TIME_TICK broadcast action.
Here's an example: https://gist.github.com/sourabh86/6826730
From the documentation:
The current time has changed. Sent every minute. You cannot receive this through components declared in manifests, only by explicitly registering for it with Context.registerReceiver()
if (time == midnight)
refreshDataManually();
Check this question out, on how to refresh your LiveData manually.
I couldn't make LiveData to properly reset, so I ended restarting activity. I decided to do so because it is pretty rare situation in my app use case.
Broadcast receiver is registered in activity onCreate, and unregistered in onDestroy.
Here is broadcast receiver that listens if date changed:
// Broadcast receiver for detecting change of day (need to refresh MainActivity)
private val mDayChangedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_DATE_CHANGED) {
isActivityRefreshNeeded = true
Log.d("mDayChangedReceiver", "Received DAY_CHANGED broadcast")
}
}
}
Then I check in onResume of MainActivity if boolean isActivityRefreshNeeded is true. If it is, I reset activity:
// If User has changed settings and navigated via back button, or day changed, refresh activity
when (isActivityRefreshNeeded) {
true -> {
isActivityRefreshNeeded = false
refreshActivity()
}
}
It is not the best solution, but as I said, it is pretty rare situation when this refresh is needed.
If someone have a better solution, I will be happy to see and implement it.
I'm still a rookie in RxJava and trying to implement something using it.
I have notifications counter in my Android application and I want to fetch the count from an API every five minutes, and I want to share this count to multiple activities. So I want to store the count in a pipeline so that each activity listens to that pipeline can get the last emitted value.
I started with an Object class and added two BehaviorSubjects one for providing the default value and the second to emit true value when the 5 minutes finish "to tell the activity that's listening if there's any" that you should fetch new count.
But the problem is that when I listen to the BehaviorSubject it emits all the data in the pipeline! How can I get only that last one
object RxBus {
#JvmStatic
var count: Int = 0
#JvmStatic
private var fetchData: Boolean = true
#JvmStatic
private var behaviorSubject: BehaviorSubject<Any> = BehaviorSubject.createDefault(CountEvent(count))
#JvmStatic
private var fetchSubject: BehaviorSubject<Any> = BehaviorSubject.createDefault(FetchEvent(fetchData))
private val FIVE_MINUTES: Long = 1000 * 60 * 5
init {
fixedRateTimer("countTimer", false, 0L, FIVE_MINUTES) {
fetchSubject.onNext(FetchEvent(true))
}
}
fun publish(event: Any) {
if (event is CountEvent) {
Log.d(TAG, "eventCount = ${event.count}")
count = event.count
fetchData = false
}
}
fun listenToLoadEvent(): Observable<FetchEvent> {
return fetchSubject.ofType(FetchEvent::class.java)
}
fun listen(): Observable<CountEvent> {
return behaviorSubject.ofType(CountEvent::class.java)
}
Please note that I want to listen to both Subjects in onResume of each activity.. Is there any way to do that in a better way? And if not can you help me to just get the last emitted item only.
Thanks in Advance
class CountEvent(val value: Int)
object CounterManager {
val counterDataSource = BehaviorProcessor.create<CountEvent>()
val refreshNotifier = interval(5, TimeUnit.MINUTES).publish()
.refCount()
fun publish(event: Any) {
if (event is CountEvent) {
counterDataSource.onNext(event)
}
}
}