Souvent, en raison des particularités du système Android et du SDK, nous devons attendre qu'une certaine partie du système soit configurée ou qu'un événement dont nous avons besoin se produise. Il s'agit souvent d'une béquille, mais parfois on ne peut pas s'en passer, surtout face aux délais. Par conséquent, de nombreux projets ont utilisé postDelayed pour cela. Sous la coupe, nous examinerons pourquoi il est si dangereux et comment y remédier.
Problème
Voyons d'abord comment postDelayed () est couramment utilisé:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.postDelayed({
Log.d("test", "postDelayed")
// do action
}, 100)
}
Cela a l'air bien, mais regardons de plus près ce code:
1) Il s'agit d'une action différée que nous attendrons pendant un certain temps. Sachant avec quelle dynamique l'utilisateur peut effectuer des transitions entre les écrans, cette action doit être annulée lors du changement d'un fragment. Cependant, cela ne se produit pas ici, et notre action sera exécutée même si le fragment actuel est détruit.
C'est facile à vérifier. Nous créons deux fragments, lors du passage au second, nous exécutons postDelayed avec un temps long, par exemple 5000 ms. Nous y retournons immédiatement. Et après un certain temps, nous voyons dans les journaux que l'action n'a pas été annulée.
2) Le second "découle" du premier. Si dans ce runnable nous passons une référence à la propriété de notre fragment, une fuite de mémoire se produira, puisque la référence au runnable vivra plus longtemps que le fragment lui-même.
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, vous pouvez connecter androidx.lifecycle: lifecycle-runtime-ktx: 2.2.0-alpha01 ou supérieur et nous aurons déjà une portée prête à l'emploi:
viewLifecycleOwner.lifecycleScope
fun Fragment.delayActionSafe(delayMillis: Long, action: () -> Unit): Job? {
view ?: return null
return viewLifecycleOwner.lifecycleScope.launch {
delay(delayMillis)
action()
}
}
RX
Dans RX, la classe Disposable est responsable de l'annulation des abonnements, mais dans RX, il n'y a pas de concurrence structurée, contrairement à coroutine. Pour cette raison, vous devez tout prescrire vous-même. Cela ressemble généralement à ceci:
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()
}
}
Nous annulons également toutes les tâches du fragment de base de la même manière:
class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes),
DisposableHolder by DisposableHolderImpl() {
override fun onDestroyView() {
super.onDestroyView()
dispose()
}
override fun onDestroy() {
super.onDestroy()
dispose()
}
}
Et l'extension elle-même:
fun BaseFragment.delayActionSafe(delayMillis: Long, block: () -> Unit): Disposable? {
view ?: return null
return Completable.timer(delayMillis, TimeUnit.MILLISECONDS).subscribe {
block()
}.also {
addDisposable(it)
}
}
En garde à vue
Lorsque vous utilisez des actions différées, nous ne devons pas oublier qu'il s'agit déjà d'une exécution asynchrone et, par conséquent, qu'elle nécessite une annulation, sinon des fuites de mémoire, des plantages et diverses autres choses inattendues commencent à se produire.