handle onKeyDown using RxAndroid - android

I am using https://github.com/mkoslacz/Moviper for my app for Android Tv Box. There is only one activity with multiple child fragments. I want to propagate the onKeyDown to child fragments only if event is not already handled. It was easy to do without using RxJava. I could return a boolean to tell that event is already handled or not. Is there a way I can handle it using RxJava? I am using Kotlin as source language. Here is something I am trying to do.
class MainActivity : ViperAiPassiveActivity<HomeContract.View>(), HomeContract.View {
private val keyPressSubject = BehaviorSubject.create<KeyEvent>()
private lateinit var predicate: (KeyEvent) -> Boolean
override fun keyPresses(predicate: (KeyEvent) -> Boolean): Observable<KeyEvent> {
this.predicate = predicate
return keyPressSubject
}
override fun createPresenter() = HomePresenter()
override fun getLayoutId(): Int {
return R.layout.activity_main
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
event?.run {
if (predicate(event)) {
keyPressSubject.onNext(event)
return true
}
}
return super.onKeyDown(keyCode, event)
}
}

I ended up creating my own Event Handler (although its not using RxJava but it does the job in my case)
object KeyEventManager {
private val handlers: MutableMap<Int, (KeyEvent?) -> Boolean> = ArrayMap()
fun register(handler: (KeyEvent?) -> Boolean): Int {
val id = (handlers.keys.max() ?: 0) + 1
handlers.put(id, handler)
return id
}
fun unregister(id: Int) {
handlers.remove(id)
}
fun postEvent(event: KeyEvent?): Boolean {
for (key in handlers.keys) {
handlers.get(key)?.run {
if (invoke(event)) {
return true
}
}
}
return false
}
}

Related

Disable firebase logging for google ML Kit library in android

Could anyone help me to tell how can I disable the firebase logging in Google ML Kit library for android. For every 15 mins it will POST some information to https://firebaselogging.googleapis.com/v0cc/log/batch?format=json_proto3
I tried using the recommendation from google https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android#kotlin+ktx and I am also not sure whether it is a right way.
Suggestions are welcome.
Google's guide from the question didn't work for me, so I have looked for alternatives.
The library is obfuscated, so it is hard to be sure, but it appears that the logging is hardcoded in. However, there is a very hacky way to disable it through some fragile reflection:
import android.util.Log
import com.google.mlkit.common.sdkinternal.LazyInstanceMap
import java.lang.reflect.Field
/**
* This class tries to disable MLKit's phoning home/logging.
* This is extremely hacky and will probably break in the next update (obfuscated class names will probably need renaming).
*
* This class exploits the fact, that there are multiple options classes which control this
* (look for "MLKitLoggingOptions" in toString implementation) and for some reason MLKit uses them as keys
* in LazyInstanceMaps which exist as static (usually) variables (which are themselves lazy).
*
* This makes sure that the LazyInstanceMaps exist, then it hijacks their internal HashMap implementation
* and replaces it with a custom map, that creates instances of whatever with logging disabled.
*
* The way to detect which holder classes need renaming, look at the stack trace, for example:
* ```
java.lang.NoClassDefFoundError: Failed resolution of: Lcom/google/android/datatransport/cct/CCTDestination;
at com.google.android.gms.internal.mlkit_vision_barcode.zznu.<init>(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:1)
at com.google.android.gms.internal.mlkit_vision_barcode.zznf.<init>(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:3)
at com.google.android.gms.internal.mlkit_vision_barcode.zznw.create(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:4)
at com.google.mlkit.common.sdkinternal.LazyInstanceMap.get(com.google.mlkit:common##18.0.0:3)
at com.google.android.gms.internal.mlkit_vision_barcode.zznx.zza(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:2)
at com.google.android.gms.internal.mlkit_vision_barcode.zznx.zzb(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:3)
at com.google.mlkit.vision.barcode.internal.zzf.create(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:3)
at com.google.mlkit.common.sdkinternal.LazyInstanceMap.get(com.google.mlkit:common##18.0.0:3)
at com.google.mlkit.vision.barcode.internal.zze.zzb(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:2)
at com.google.mlkit.vision.barcode.BarcodeScanning.getClient(com.google.android.gms:play-services-mlkit-barcode-scanning##18.0.0:3)
* ```
* here are two LazyInstanceMap lookups, of which only the second one (through trial and error or with debugger)
* uses MLKitLoggingOptions keys. From here we can find that the holder class is com.google.android.gms.internal.mlkit_vision_barcode.zznx .
*/
object MLKitTrickery {
private class mlkit_vision_barcodeLoggingOptions(base: com.google.android.gms.internal.mlkit_vision_barcode.zzne) : com.google.android.gms.internal.mlkit_vision_barcode.zzne() {
private val libraryName: String = base.zzb()
private val firelogEventType: Int = base.zza()
override fun zza(): Int = firelogEventType
override fun zzb(): String = libraryName
override fun zzc(): Boolean = false //enableFirelog
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as mlkit_vision_barcodeLoggingOptions
if (libraryName != other.libraryName) return false
if (firelogEventType != other.firelogEventType) return false
return true
}
override fun hashCode(): Int {
var result = libraryName.hashCode()
result = 31 * result + firelogEventType
return result
}
}
private class mlkit_vision_commonLoggingOptions(base: com.google.android.gms.internal.mlkit_vision_common.zzjn) : com.google.android.gms.internal.mlkit_vision_common.zzjn() {
private val libraryName: String = base.zzb()
private val firelogEventType: Int = base.zza()
override fun zza(): Int = firelogEventType
override fun zzb(): String = libraryName
override fun zzc(): Boolean = false //enableFirelog
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as mlkit_vision_commonLoggingOptions
if (libraryName != other.libraryName) return false
if (firelogEventType != other.firelogEventType) return false
return true
}
override fun hashCode(): Int {
var result = libraryName.hashCode()
result = 31 * result + firelogEventType
return result
}
}
private fun isMLKitLoggingOptions(obj: Any): Boolean {
return obj is com.google.android.gms.internal.mlkit_vision_barcode.zzne
|| obj is com.google.android.gms.internal.mlkit_vision_common.zzjn
}
private fun convertMLKitLoggingOptions(obj: Any): Any? {
if (obj is com.google.android.gms.internal.mlkit_vision_barcode.zzne) {
return mlkit_vision_barcodeLoggingOptions(obj)
}
if (obj is com.google.android.gms.internal.mlkit_vision_common.zzjn) {
return mlkit_vision_commonLoggingOptions(obj)
}
return null
}
#Suppress("UNCHECKED_CAST")
private fun patchLazyMap(lazyMapHolder:Any?, lazyMapHolderClass: Class<*>) {
val holderField = lazyMapHolderClass.declaredFields.find { LazyInstanceMap::class.java.isAssignableFrom(it.type) }!!
var currentLazyInstanceMap = holderField.get(lazyMapHolder)
if (currentLazyInstanceMap == null) {
var lastError: Throwable? = null
for (constructor in holderField.type.declaredConstructors) {
try {
constructor.isAccessible = true
val params = arrayOfNulls<Any?>(constructor.parameterCount)
currentLazyInstanceMap = constructor.newInstance(*params)
holderField.set(lazyMapHolder, currentLazyInstanceMap)
} catch (e:Throwable) {
lastError = e
}
}
if (currentLazyInstanceMap == null) {
throw java.lang.Exception("Failed to initialize LazyInstanceMap "+holderField.type, lastError)
}
}
var mapHolderClass: Class<*> = currentLazyInstanceMap.javaClass
val createMethod = mapHolderClass.getDeclaredMethod("create", Object::class.java)
val mapField: Field
while (true) {
val mapFieldCandidate = mapHolderClass.declaredFields.firstOrNull { Map::class.java.isAssignableFrom(it.type) }
if (mapFieldCandidate != null) {
mapField = mapFieldCandidate
break
}
mapHolderClass = mapHolderClass.superclass ?: error("It appears that ${currentLazyInstanceMap.javaClass} does not have a backing map field")
}
val oldMap = mapField.get(currentLazyInstanceMap) as MutableMap<Any, Any?>
val customMap = object : MutableMap<Any, Any?> by oldMap {
override fun containsKey(key: Any): Boolean {
if (oldMap.containsKey(key)) {
return true
}
if (isMLKitLoggingOptions(key)) {
return true
}
return false
}
override fun get(key: Any): Any? {
val existing = oldMap.get(key)
if (existing != null) {
return existing
}
val convertedKey = convertMLKitLoggingOptions(key)
if (convertedKey != null) {
val created = createMethod.invoke(currentLazyInstanceMap, convertedKey)
oldMap.put(key, created)
return created
}
return null
}
}
mapField.isAccessible = true
mapField.set(currentLazyInstanceMap, customMap)
}
private var initialized = false
/**
* Call this to attempt to disable MLKit logging.
*/
fun init() {
try {
patchLazyMap(null, com.google.android.gms.internal.mlkit_vision_barcode.zznx::class.java)
patchLazyMap(null, com.google.android.gms.internal.mlkit_vision_common.zzkc::class.java)
initialized = true
} catch (e: Throwable) {
Log.e("MLKitTrickery", "Failed to disable MLKit phoning home")
}
}
}
When you also shim out GMS TelemetryLogging with:
#file:Suppress("unused", "UNUSED_PARAMETER")
package com.google.android.gms.common.internal
import android.app.Activity
import android.content.Context
import android.os.Parcel
import com.google.android.gms.tasks.OnFailureListener
import com.google.android.gms.tasks.OnSuccessListener
import com.google.android.gms.tasks.Task
import java.util.concurrent.Executor
class TelemetryLoggingOptions {
class Builder {
fun setApi(api: String?): Builder = this
fun build(): TelemetryLoggingOptions = TelemetryLoggingOptions()
}
companion object {
#JvmStatic
fun builder(): Builder = Builder()
}
}
private object DummyLogTask : Task<Void?>() {
override fun addOnFailureListener(p0: OnFailureListener): Task<Void?> {
// Implemented, because failing tells MLKit to back-off for 30 minutes, which is a win for performance
p0.onFailure(exception)
return this
}
override fun addOnFailureListener(p0: Activity, p1: OnFailureListener): Task<Void?> = addOnFailureListener(p1)
override fun addOnFailureListener(p0: Executor, p1: OnFailureListener): Task<Void?> = addOnFailureListener(p1)
override fun addOnSuccessListener(p0: OnSuccessListener<in Void?>): Task<Void?> = this
override fun addOnSuccessListener(p0: Activity, p1: OnSuccessListener<in Void?>): Task<Void?> = addOnSuccessListener(p1)
override fun addOnSuccessListener(p0: Executor, p1: OnSuccessListener<in Void?>): Task<Void?> = addOnSuccessListener(p1)
override fun getException(): Exception? = exception
override fun getResult(): Void? = null
override fun <X : Throwable?> getResult(p0: Class<X>): Void? = null
override fun isCanceled(): Boolean = false
override fun isComplete(): Boolean = true
override fun isSuccessful(): Boolean = false
private val exception = Exception("Success was never an option")
}
object TelemetryLogging {
#JvmStatic
fun getClient(context: Context): TelemetryLoggingClient {
return object : TelemetryLoggingClient {
override fun log(data: TelemetryData): Task<Void?> {
return DummyLogTask
}
}
}
#JvmStatic
fun getClient(context: Context, options: TelemetryLoggingOptions): TelemetryLoggingClient {
return getClient(context)
}
}
interface TelemetryLoggingClient {
fun log(data: TelemetryData): Task<Void?>
}
class TelemetryData(var1: Int, var2:List<MethodInvocation>?) {
fun writeToParcel(var1: Parcel, var2: Int) {}
}
class MethodInvocation {
constructor(methodKey:Int, resultStatusCode:Int, connectionResultStatusCode:Int,
startTimeMillis:Long, endTimeMillis:Long,
callingModuleId: String?, callingEntryPoint: String?, serviceId:Int)
constructor(methodKey:Int, resultStatusCode:Int, connectionResultStatusCode:Int,
startTimeMillis:Long, endTimeMillis:Long,
callingModuleId: String?, callingEntryPoint: String?,
serviceId:Int, var11:Int)
fun writeToParcel(var1: Parcel, var2: Int) {}
}
it is possible to trim many transitive dependencies and save apk size:
implementation("com.google.mlkit:barcode-scanning:17.0.2") {
exclude("com.google.android.gms", "play-services-base")
exclude("com.google.android.datatransport", "transport-api")
exclude("com.google.android.datatransport", "transport-backend-cct")
exclude("com.google.android.datatransport", "transport-runtime")
exclude("com.google.firebase", "firebase-encoders-json")
exclude("com.google.firebase", "firebase-encoders")
}
However, as noted above, this is very fragile and will probably somehow break after MLKit update. It would be nice if this was not needed.

Android Recycleview DiffUtil doesn't work properly

I am using recycle view with diffutil in my application. but while I rotating or comeback from another screen the adapter gets updated. why is this happening?.
Here My ViewModel
class FeedsViewModel() : ViewModel() {
private val feedsRepository = FeedsRepository()
val feedsLiveData: MutableLiveData<Resource<UserFeeds>> = MutableLiveData()
init {
val apiParams = HashMap<String, String>()
apiParams["user_id"] = "1"
getFeeds(apiParams,"123"
}
fun getFeeds(apiParams: HashMap<String, String>, token: String) = viewModelScope.launch {
feedsLiveData.postValue(Resource.Loading())
val response = feedsRepository.getFeeds(apiParams, token)
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
feedsLiveData.postValue(Resource.Success(resultResponse))
}
} else {
feedsLiveData.postValue(Resource.Error(response.message()))
}
}
}
I am using fragment to display it
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerViewFeeds.adapter = feedsAdapter
viewModel.feedsLiveData.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
binding.progressBar.visibility = View.GONE
response.data?.let { userFeeds ->
feedsAdapter.differ.submitList(userFeeds.userPosts.toList())
binding.nooFeeds.visibility = View.GONE
}
is Resource.Error -> {....}
is Resource.Loading -> {....}
}
})
}
and my adapter
class FeedsAdapter(private val context: Context, private val itemClickListener: FeedsItemCallBack) :
RecyclerView.Adapter<FeedsAdapter.MyViewHolder>() {
class MyViewHolder(val bindin: ItemViewFeedsBinding) : RecyclerView.ViewHolder(bindin.root) {
}
private val differCallback = object : DiffUtil.ItemCallback<UserPost>() {
override fun areItemsTheSame(oldItem: UserPost, newItem: UserPost): Boolean {
return oldItem.postId == newItem.postId
}
override fun areContentsTheSame(oldItem: UserPost, newItem: UserPost): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
var feedsItem = differ.currentList[position]
holder.bindin.feedData = feedsItem;
holder.bindin.executePendingBindings()
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
Is this implementation is correct?.
Is this issue of ViewModel or adapter?
please help. Thanks in advance
you could distinguish the cases of your activity being created for
the first time and being restored from savedInstanceState. This is
done by overriding onSaveInstanceState and checking the parameter of
onCreate.
You could lock the activity in one orientation by adding
android:screenOrientation="portrait" (or "landscape") to
in your manifest.
You could tell the system that you meant to handle screen changes
for yourself by specifying
android:configChanges="orientation|screenSize" in the
tag. This way the activity will not be recreated, but will receive a
callback instead (which you can ignore as it's not useful for you).
Personally, I'd go with (3). Of course if locking the app to one of the orientations is fine with you, you can also go with (2).

Returning value from normal function which called suspend function using Kotlin Coroutine

Hi I am using Kotlin coroutine library in a project.
The below method calls a suspend function which return a boolean value.
fun isNetworkAvailable(context: Context?): Boolean {
//return checkNetworkReachability(context)
var isNetworkAvailable = false
GlobalScope.launch(Dispatchers.Default) {
isNetworkAvailable = GlobalScope.async<Boolean> {
checkNetwork()
}.await()
}
return isNetworkAvailable
}
Here checkNetwork is the suspend function. Before executing it the return value is passed to the caller (View/Activity).
How could I achieve without making the "isNetworkAvailable" as suspend?.
Inside the checkNetwork method, to check reach-ability calling network call as like below.
private suspend fun checkNetwork() : Boolean {
val value = GlobalScope.async<Boolean> {
val isEastReachable = async { checkEastReachable() }
if (!isEastReachable.await()) {
checkWestReachable()
} else {
true
}
}
return value.await()
}
And the sub-methods are
private suspend fun checkEastReachable(): Boolean = coroutineScope {
withContext(Dispatchers.Default) {
repository.networkManager.callReachableEast()
}
}
private suspend fun checkWestReachable(): Boolean = coroutineScope {
withContext(Dispatchers.Default) {
repository.networkManager.callReachableWest()
}
}
The sub-suspend methods are calling a web service using retrofit. As it would return a boolean, I made it as an synchronous .execute() call.
fun callReachableEast(): Boolean {
return try {
val requestCall =
ApiService.create("eastApi").getReachabilityEast()
requestCall.execute().isSuccessful
} catch (exception: Exception) {
false
}
}
fun callReachableWest(): Boolean {
return try {
val requestCall =
ApiService.create("westApi").getReachabilityWest()
return requestCall.execute().isSuccessful
} catch (exception: Exception) {
false
}
}
I have gone through the below links
https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html
https://proandroiddev.com/async-operations-with-kotlin-coroutines-part-1-c51cc581ad33
and some more.
Repeating my question, How could I achieve without making the "isNetworkAvailable" as suspend?.
If you can't make isNetworkAvailable a suspend function, then it will be a blocking function. This means, any code calling isNetworkAvailable will block as well, or you'd need to change this function's signature to have a callback instead.
First, let's look at the blocking version. There is a special coroutine-builder that is suited for bridging from the suspendable world into the regular/blocking world. It is called runBlocking:
fun isNetworkAvailable(context: Context?): Boolean = runBlocking {
checkNetworkReachability(context)
}
...
val isAvailable = isNetworkAvailable(activity)
if (isAvailable) { ... }
...
If you'd like to change its signature and have a callback instead of a return-value:
fun CoroutineScope.isNetworkAvailable(context: Context?, callback: (Boolean) -> Unit) {
launch {
callback(checkNetworkReachability(context))
}
}
...
scope.isNetworkAvailable(activity) { isAvailable ->
if (isAvailable) { ... }
}
(code may have typos)

update() not being called inside Activity which is Observer

My MainActivity implements the Observer class. I also have a class called ObservedObject that extends the Observable class.
Here is my custom Observable , called ObservedObject:
class ObservedObject(var value: Boolean) : Observable() {
init {
value = false
}
fun setVal(vals: Boolean) {
value = vals
setChanged()
notifyObservers()
}
fun printVal() {
Log.i("Value" , "" + value)
}
}
Here is my Application called SpeechApp which contains my ObservedObject (an Observable actually):
class SpeechApp: Application() {
var isDictionaryRead = ObservedObject(false)
override fun onCreate() {
super.onCreate()
wordslist = ArrayList()
Thread {
execute()
}.start()
}
fun execute() {
while (/* Condition */) {
//Log.i("Read" , line)
/*Does Something Here*/
}
isDictionaryRead.setVal(true)
}
}
In my MainActivity, I mainly have a dialog, that should be displayed after I have got the output after Speech Recognition. It will display as long as the value of isDictionaryRead doesn't change to true:
class MainActivity(private val REQ_CODE_SPEECH_INPUT: Int = 100) : AppCompatActivity() , Observer{
override fun update(o: Observable?, arg: Any?) {
(o as ObservedObject).printVal()
dialog.hide()
}
private lateinit var app : SpeechApp
private lateinit var dialog: MaterialDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
dialog = MaterialDialog.Builder(this)
.title("Please Wait")
.content("Loading from the Dictionary")
.progress(true , 0)
.build()
app = application as SpeechApp
app.isDictionaryRead.addObserver(this)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_speech, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
val id = item?.itemId
when(id) {
R.id.menu_option_speech -> {
invokeSpeech()
}
}
return super.onOptionsItemSelected(item)
}
private fun invokeSpeech() {
/* Does Something, Works Fine */
try {
startActivityForResult(intent , REQ_CODE_SPEECH_INPUT)
}
catch (ex: ActivityNotFoundException) {
/* Does Something */
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQ_CODE_SPEECH_INPUT -> {
if (resultCode == Activity.RESULT_OK && null != data) {
dialog.show()
}
}
}
super.onActivityResult(requestCode, resultCode, data)
}
}
Now the problem is, when the SpeechApp sets the value of isDictionaryRead to true, I expect it to call the MainActivity update() method, wherein I have given the code to hide the dialog. That particular code is not working, and my dialog box doesn't go away. Where am I going wrong?
PS. I've pushed my code to Github now, just in case anyone could help me where I am going wrong.
The only thing I can think of that would cause this problem is that the execute() thread that was started in SpeechApp.onCreate finished execution and called isDictionaryRead.setVal(true) before the activity could call app.isDictionaryRead.addObserver(this). As a result, notifyObservers is called before the activity even starts observing, and as a result it is not notified. Here's my proposed solution: Start the execute thread in the activity's onCreate method after adding it as an observer.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
dialog = MaterialDialog.Builder(this)
.title("Please Wait")
.content("Loading from the Dictionary")
.progress(true , 0)
.build()
app = application as SpeechApp
app.isDictionaryRead.addObserver(this)
app.asyncReadDictionary()
}
Then remove the thread call from SpeechApp.onCreate and use this instead
// in SpeechApp
fun asyncReadDictionary() {
if (!isDictionaryRead.value) {
Thread { execute() }.start()
}
}
private fun execute() {
while (/* Condition */) {
//Log.i("Read" , line)
/*Does Something Here*/
}
isDictionaryRead.value = true
}
Also, reimplement ObservableObject as follows
class ObservedObject : Observable() {
var value: Boolean = false
set(newValue) {
field = newValue
setChanged()
notifyObservers()
}
fun printVal() {
Log.i("Value" , "" + value)
}
}

Multiple calls to set LiveData is not observed

I have recently seen a weird issue that is acting as a barrier to my project.
Multiple calls to set the live data value does not invoke the observer in the view.
It seems that only the last value that was set actually invokes the Observer in the view.
Here is the code snippet for a review.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this).get(MainViewModelImpl::class.java)
viewModel.state().observe(this, Observer {
onStateChange(it!!)
})
viewModel.fetchFirstThree()
}
private fun onStateChange(state: MainViewModel.State) {
when (state) {
is One -> {
show(state.data)
}
is Two -> {
show(state.data)
}
is Three -> {
show(state.data)
}
}
}
private fun show(data: String) {
Log.d("Response", data)
}
}
MainViewModel.kt
abstract class MainViewModel : ViewModel() {
sealed class State {
data class One(val data: String) : State()
data class Two(val data: String) : State()
data class Three(val data: String) : State()
}
abstract fun state(): LiveData<State>
abstract fun fetchFirstThree()
}
MainViewModelImpl.kt
class MainViewModelImpl : MainViewModel() {
private val stateLiveData: MediatorLiveData<State> = MediatorLiveData()
override fun state(): LiveData<State> = stateLiveData
override fun fetchFirstThree() {
stateLiveData.value = State.One("One")
stateLiveData.value = State.Two("Two")
stateLiveData.value = State.Three("Three")
}
}
Expected output:
Response: One
Response: Two
Response: Three
Actual Output:
Response: Three
As per the output above, the Observer is not being called for the first two values.
I did some science, re-implementing LiveData and MutableLiveData to log out some data.
Check the source code here.
setValue value=Test1
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
Returned at !observer.active
setValue value=Test2
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
Returned at !observer.active
setValue value=Test3
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
Returned at !observer.active
dispatchingValue mDispatchingValue=false mDispatchInvalidated=false
considerNotify
ITEM: Test3
It looks like the observer hasn't reached an active state when you send the initial values.
private void considerNotify(LifecycleBoundObserver observer) {
// <-- Three times it fails here. This means that your observer wasn't ready for any of them.
if (!observer.active) {
return;
}
Once the observer reaches an active state, it sends the last set value.
void activeStateChanged(boolean newActive) {
if (newActive == active) {
return;
}
active = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += active ? 1 : -1;
if (wasInactive && active) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !active) {
onInactive();
}
if (active) {
// <--- At this point you are getting a call to your observer!
dispatchingValue(this);
}
}
I had such issue too.
To resolve it was created custom MutableLiveData, that contains a queue of posted values and will notify observer for each value.
You can use it the same way as usual MutableLiveData.
open class MultipleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
private val values: Queue<T> = LinkedList()
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(this::class.java.name, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, { t: T ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
//call next value processing if have such
if (values.isNotEmpty())
pollValue()
}
})
}
override fun postValue(value: T) {
values.add(value)
pollValue()
}
private fun pollValue() {
value = values.poll()
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#Suppress("unused")
#MainThread
fun call() {
value = null
}
}
You could use custom LiveData like this:
class ActiveMutableLiveData<T> : MutableLiveData<T>() {
private val values: Queue<T> = LinkedList()
private var isActive: Boolean = false
override fun onActive() {
isActive = true
while (values.isNotEmpty()) {
setValue(values.poll())
}
}
override fun onInactive() {
isActive = false
}
override fun setValue(value: T) {
if (isActive) {
super.setValue(value)
} else {
values.add(value)
}
}
}
FWIW I had the same problem but solved it like this...
I originally had some code similar to this...
private fun updateMonth(month: Int){
updateMonth.value = UpdateMonth(month, getDaysOfMonth(month))
}
updateMonth(1)
updateMonth(2)
updateMonth(3)
I experienced the same problem as described...
But when I made this simple change....
private fun updateMonth(month: Int) {
CoroutineScope(Dispatchers.Main).launch {
updateMonth.value = UpdateMonth(month, getDaysOfMonth(month))
}
}
Presumably, each updateMonth is going onto a different thread now, so all of the updates are observed.
You should call viewModel.fetchFirstThree() after Activity's onStart() method. for example in onResume() method.
Because in LiveData the Observer is wrapped as a LifecycleBoundObserver. The field mActive set to true after onStart().
class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
#Override
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);// return true after onStart()
}
#Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());// after onStart() change mActive to true
}
}
When the observer notify the change it calls considerNotify, before onStart it will return at !observer.mActive
private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {// called in onCreate() will return here.
return;
}
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}

Categories

Resources