Often, due to the peculiarities of the android system and sdk, we need to wait until a certain part of the system is configured or some event we need occurs. This is often a crutch, but sometimes you can't do without it, especially in the face of deadlines. Therefore, many projects used postDelayed for this. Under the cut, we will consider why he is so dangerous and what to do about it.
Problem
First, let's look at how postDelayed () is commonly used:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.postDelayed({
Log.d("test", "postDelayed")
// do action
}, 100)
}
It looks good, but let's take a closer look at this code:
1) This is a deferred action that we will wait for some time to complete. Knowing how dynamically the user can make transitions between screens, this action should be canceled when changing a fragment. However, this does not happen here, and our action will be executed even if the current fragment is destroyed.
It's easy to check. We create two fragments, when switching to the second, we run postDelayed with a long time, for example 5000 ms. We immediately go back. And after a while we see in the logs that the action has not been canceled.
2) The second "follows" from the first. If in this runnable we pass a reference to the property of our fragment, a memory leak will occur, since the reference to the runnable will live longer than the fragment itself.
3) :
, view onDestroyView
synthitec - java.lang.NullPointerException
, _$_clearFindViewByIdCache
, findViewById
null
viewBinding - java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null
?
- view — doOnLayout doOnNextLayout
- , - (Presenter/ViewModel - ). .
- .
, view window.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Runnable {
// do action
}.let { runnable ->
view.postDelayed(runnable, 100)
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) {}
override fun onViewDetachedFromWindow(view: View) {
view.removeOnAttachStateChangeListener(this)
view.removeCallbacks(runnable)
}
})
}
}
doOnDetach , view window, onViewCreated. .
View.kt:
inline fun View.doOnDetach(crossinline action: (view: View) -> Unit) {
if (!ViewCompat.isAttachedToWindow(this)) { //
action(this) //
} else {
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) {}
override fun onViewDetachedFromWindow(view: View) {
removeOnAttachStateChangeListener(this)
action(view)
}
})
}
}
extension:
fun View.postDelayedSafe(delayMillis: Long, block: () -> Unit) {
val runnable = Runnable { block() }
postDelayed(runnable, delayMillis)
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) {}
override fun onViewDetachedFromWindow(view: View) {
removeOnAttachStateChangeListener(this)
view.removeCallbacks(runnable)
}
})
}
. . , . Native Android 2 — Rx Coroutines.
.
, 100% . //.
Coroutines
, di . :
class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes), CoroutineScope by MainScope() {
override fun onDestroyView() {
super.onDestroyView()
coroutineContext[Job]?.cancelChildren()
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
onDestroyView, scope, View Fragment. Fragment .
onDestroy scope, .
.
postDelayed:
fun BaseFragment.delayActionSafe(delayMillis: Long, action: () -> Unit): Job? {
view ?: return null
return launch {
delay(delayMillis)
action()
}
}
, , view , null. . view, .
Keanu_Reeves, you can connect androidx.lifecycle: lifecycle-runtime-ktx: 2.2.0-alpha01 or higher and we will already have a ready-made scope:
viewLifecycleOwner.lifecycleScope
fun Fragment.delayActionSafe(delayMillis: Long, action: () -> Unit): Job? {
view ?: return null
return viewLifecycleOwner.lifecycleScope.launch {
delay(delayMillis)
action()
}
}
RX
In RX, the Disposable class is responsible for canceling subscriptions, but in RX there is no Structured concurrency, unlike coroutine. Because of this, you have to prescribe it all yourself. It usually looks like this:
interface DisposableHolder {
fun dispose()
fun addDisposable(disposable: Disposable)
}
class DisposableHolderImpl : DisposableHolder {
private val compositeDisposable = CompositeDisposable()
override fun addDisposable(disposable: Disposable) {
compositeDisposable.add(disposable)
}
override fun dispose() {
compositeDisposable.clear()
}
}
We also cancel all tasks in the base fragment in the same way:
class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes),
DisposableHolder by DisposableHolderImpl() {
override fun onDestroyView() {
super.onDestroyView()
dispose()
}
override fun onDestroy() {
super.onDestroy()
dispose()
}
}
And the extension itself:
fun BaseFragment.delayActionSafe(delayMillis: Long, block: () -> Unit): Disposable? {
view ?: return null
return Completable.timer(delayMillis, TimeUnit.MILLISECONDS).subscribe {
block()
}.also {
addDisposable(it)
}
}
In custody
When using any deferred actions, we must not forget that this is already an asynchronous execution, and accordingly it requires cancellation, otherwise memory leaks, crashes and various other unexpected things begin to occur.