Fixed
Status Update
Comments
se...@google.com <se...@google.com> #2
The problem is reproducible using support lib 26.1.0, this is the updated activity:
class LiveDataTestActivity : AppCompatActivity() {
private val myLiveData = MutableLiveData<Int>().apply { value = 0 }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textView = TextView(this).apply {
text = "AAAA"
setOnClickListener {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "This is my text to send.")
type = "text/plain"
}
startActivityForResult(sendIntent, 123)
}
}
setContentView(textView)
myLiveData.observe(this, Observer<Int> {
textView.text = it.toString()
})
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
myLiveData.value = (myLiveData.value ?: 0) + 1
println("Value ${myLiveData.value}")
}
override fun onResume() {
super.onResume()
println("resume ${lifecycle.currentState}")
}
}
class LiveDataTestActivity : AppCompatActivity() {
private val myLiveData = MutableLiveData<Int>().apply { value = 0 }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textView = TextView(this).apply {
text = "AAAA"
setOnClickListener {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "This is my text to send.")
type = "text/plain"
}
startActivityForResult(sendIntent, 123)
}
}
setContentView(textView)
myLiveData.observe(this, Observer<Int> {
textView.text = it.toString()
})
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
myLiveData.value = (myLiveData.value ?: 0) + 1
println("Value ${myLiveData.value}")
}
override fun onResume() {
super.onResume()
println("resume ${lifecycle.currentState}")
}
}
yb...@google.com <yb...@google.com> #3
thanks for the reports.
After further investigation:
good news, this is not a very serious bug, it recovers right after onResume. (also the bug does not happen in latest versions of the OS)
bad news, properly fixing it requires changes in the support library so it may not catch 1.0.0 release.
details:
Lifecycles moves state to CREATED when onSavedInstanceState is called. This is done because FragmentManager disables fragment transactions when onSavedInstanceState is called. Lifecycles API guaratees that you can run a fragment transaction inside an observer. This behavior is usually fine because onSavedInstanceState is always followed by onStop.
Turns out, this is not the case for API21..
things to do:
* make sure we have tests that fragments are handled properly and observers don't receive an unexpected STARTED event.
* in the next support lib version, make sure the state is set to at least STARTED when FragmentController#noteStateNotSaved is called.
After further investigation:
good news, this is not a very serious bug, it recovers right after onResume. (also the bug does not happen in latest versions of the OS)
bad news, properly fixing it requires changes in the support library so it may not catch 1.0.0 release.
details:
Lifecycles moves state to CREATED when onSavedInstanceState is called. This is done because FragmentManager disables fragment transactions when onSavedInstanceState is called. Lifecycles API guaratees that you can run a fragment transaction inside an observer. This behavior is usually fine because onSavedInstanceState is always followed by onStop.
Turns out, this is not the case for API21..
things to do:
* make sure we have tests that fragments are handled properly and observers don't receive an unexpected STARTED event.
* in the next support lib version, make sure the state is set to at least STARTED when FragmentController#noteStateNotSaved is called.
yb...@google.com <yb...@google.com> #4
Fabio, thanks for the bug report and example code. We have a fix in the works that will update the TextView on 5.0 (the problem actually existed on 6.0 and below) just like it should on 6.0.
However, note that on 6.0 and below your onResume method will continue to print "CREATED" as Yigit mentioned above.
Thanks again!
However, note that on 6.0 and below your onResume method will continue to print "CREATED" as Yigit mentioned above.
Thanks again!
yb...@google.com <yb...@google.com> #5
reopened. fix for live data is in but the greater fix to make lifecycles consistent is still not merged.
da...@gmail.com <da...@gmail.com> #6
Norm
yb...@google.com <yb...@google.com> #7
Fragment ransaction bug:
UncaughtException
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
at android.view.ViewGroup.addViewInner(ViewGroup.java:4937)
at android.view.ViewGroup.addView(ViewGroup.java:4768)
at android.view.ViewGroup.addView(ViewGroup.java:4708)
at android.view.ViewGroup.addView(ViewGroup.java:4681)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1425)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1750)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1819)
at android.support.v4.app.BackStackRecord.executePopOps(BackStackRecord.java:855)
at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2587)
at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2377)
at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2332)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2239)
at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:700)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
UncaughtException
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
at android.view.ViewGroup.addViewInner(ViewGroup.java:4937)
at android.view.ViewGroup.addView(ViewGroup.java:4768)
at android.view.ViewGroup.addView(ViewGroup.java:4708)
at android.view.ViewGroup.addView(ViewGroup.java:4681)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1425)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1750)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1819)
at android.support.v4.app.BackStackRecord.executePopOps(BackStackRecord.java:855)
at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2587)
at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2377)
at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2332)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2239)
at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:700)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
ap...@google.com <ap...@google.com> #8
Project: platform/frameworks/support
Branch: androidx-master-dev
commit af12e75e6b4110f48e44ca121466943909de8f06
Author: Yigit Boyar <yboyar@google.com>
Date: Tue Sep 03 12:58:11 2019
Fix coroutine livedata race condition
This CL fixes a bug in liveData builder where emitting same
LiveData source twice would make it crash because the second
emission registry could possibly happen before first one is
removed as source.
We fix it by using a suspending dispose function. It does feel
a bit hacky but we cannot make DisposableHandle.dispose async
and we do not want to block there. This does not mean that there
is a problem if developer disposes it manually since our emit
functions take care of making sure it disposes (and there is
no other way to add source to the underlying MediatorLiveData)
Bug: 140249349
Test: BuildLiveDataTest#raceTest_*
Change-Id: I0b464c242a583da4669af195cf2504e2adc4de40
M lifecycle/lifecycle-livedata-ktx/api/2.2.0-alpha05.txt
M lifecycle/lifecycle-livedata-ktx/api/current.txt
M lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_2.2.0-alpha05.txt
M lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_current.txt
M lifecycle/lifecycle-livedata-ktx/api/restricted_2.2.0-alpha05.txt
M lifecycle/lifecycle-livedata-ktx/api/restricted_current.txt
M lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
M lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt
https://android-review.googlesource.com/1112186
https://goto.google.com/android-sha1/af12e75e6b4110f48e44ca121466943909de8f06
Branch: androidx-master-dev
commit af12e75e6b4110f48e44ca121466943909de8f06
Author: Yigit Boyar <yboyar@google.com>
Date: Tue Sep 03 12:58:11 2019
Fix coroutine livedata race condition
This CL fixes a bug in liveData builder where emitting same
LiveData source twice would make it crash because the second
emission registry could possibly happen before first one is
removed as source.
We fix it by using a suspending dispose function. It does feel
a bit hacky but we cannot make DisposableHandle.dispose async
and we do not want to block there. This does not mean that there
is a problem if developer disposes it manually since our emit
functions take care of making sure it disposes (and there is
no other way to add source to the underlying MediatorLiveData)
Bug: 140249349
Test: BuildLiveDataTest#raceTest_*
Change-Id: I0b464c242a583da4669af195cf2504e2adc4de40
M lifecycle/lifecycle-livedata-ktx/api/2.2.0-alpha05.txt
M lifecycle/lifecycle-livedata-ktx/api/current.txt
M lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_2.2.0-alpha05.txt
M lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_current.txt
M lifecycle/lifecycle-livedata-ktx/api/restricted_2.2.0-alpha05.txt
M lifecycle/lifecycle-livedata-ktx/api/restricted_current.txt
M lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
M lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt
Description
Version used: 2.2.0-alpha01
Devices/Android versions reproduced on: Android 9
I'm having some random crashes due to CoroutineLiveData
```
Fatal Exception: java.lang.IllegalArgumentException: This source was already added with the different observer
at androidx.lifecycle.MediatorLiveData.addSource + 89(MediatorLiveData.java:89)
at androidx.lifecycle.CoroutineLiveDataKt.addDisposableSource + 102(CoroutineLiveDataKt.java:102)
at androidx.lifecycle.CoroutineLiveData.emitSource$lifecycle_livedata_ktx_release + 200(CoroutineLiveData.java:200)
at androidx.lifecycle.LiveDataScopeImpl$emitSource$2.invokeSuspend + 89(LiveDataScopeImpl.java:89)
at androidx.lifecycle.LiveDataScopeImpl$emitSource$2.invoke(LiveDataScopeImpl.java:11)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn + 91(UndispatchedKt.java:91)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext + 156(BuildersKt__Builders_commonKt.java:156)
at kotlinx.coroutines.BuildersKt.withContext + 1(BuildersKt.java:1)
at androidx.lifecycle.LiveDataScopeImpl.emitSource + 88(LiveDataScopeImpl.java:88)
at com.geekorum.ttrss.articles_list.FeedsViewModel$refreshed$1.invokeSuspend + 90(FeedsViewModel.java:90)
at com.geekorum.ttrss.articles_list.FeedsViewModel$refreshed$1.invoke(FeedsViewModel.java:8)
at androidx.lifecycle.BlockRunner$maybeRun$1.invokeSuspend + 147(BlockRunner.java:147)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith + 33(BaseContinuationImpl.java:33)
at kotlinx.coroutines.DispatchedTask.run + 241(DispatchedTask.java:241)
at android.os.Handler.handleCallback + 873(Handler.java:873)
at android.os.Handler.dispatchMessage + 99(Handler.java:99)
at android.os.Looper.loop + 193(Looper.java:193)
at android.app.ActivityThread.main + 6898(ActivityThread.java:6898)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run + 537(RuntimeInit.java:537)
at com.android.internal.os.ZygoteInit.main + 858(ZygoteInit.java:858)
```
From what I understand of CoroutineLiveData source code, when calling `LiveDataScope.emitSource()` the previous source is removed before adding the new one on MediatorLiveData.
```
@MainThread
internal fun emitSource(source: LiveData<T>): DisposableHandle {
clearSource()
val newSource = addDisposableSource(source)
emittedSource = newSource
return newSource
}
```
However the removing is done by launching a new coroutine
```
internal fun <T> MediatorLiveData<T>.addDisposableSource(
source: LiveData<T>
): DisposableHandle {
val disposed = AtomicBoolean(false)
addSource(source) {
if (!disposed.get()) {
value = it
} else {
removeSource(source)
}
}
return object : DisposableHandle {
override fun dispose() {
if (disposed.compareAndSet(false, true)) {
CoroutineScope(Dispatchers.Main).launch {
removeSource(source)
}
}
}
}
}
```
So there is no guarantee that `MediatorLiveData.addSource()` will be called after the removing coroutine is executed. This leads to the IllegalArgumentException in `MediatorLiveData.addSource()`
```
@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
Source<S> e = new Source<>(source, onChanged);
Source<?> existing = mSources.putIfAbsent(source, e);
if (existing != null && existing.mObserver != onChanged) {
throw new IllegalArgumentException(
"This source was already added with the different observer");
}
if (existing != null) {
return;
}
if (hasActiveObservers()) {
e.plug();
}
}
```