Freqüentemente, devido às peculiaridades do sistema android e sdk, precisamos esperar até que uma determinada parte do sistema seja configurada ou algum evento de que necessitamos ocorra. Muitas vezes, é uma muleta, mas às vezes você não pode ficar sem ela, especialmente em um ambiente de prazos. Portanto, muitos projetos usados postDelayed para isso. Abaixo do corte, consideraremos por que ele é tão perigoso e o que fazer a respeito.
Problema
Primeiro, vamos ver como postDelayed () é comumente usado:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.postDelayed({
Log.d("test", "postDelayed")
// do action
}, 100)
}
Parece bom, mas vamos dar uma olhada mais de perto neste código:
1) Esta é uma ação adiada que esperaremos algum tempo para ser concluída. Sabendo o quão dinamicamente o usuário pode fazer transições entre telas, esta ação deve ser cancelada ao alterar um fragmento. Porém, isso não acontece aqui, e nossa ação será executada mesmo se o fragmento atual for destruído.
É fácil verificar. Criamos dois fragmentos, ao mudar para o segundo, executamos postDelayed com muito tempo, por exemplo 5000 ms. Nós imediatamente voltamos. E depois de um tempo, vemos nos logs que a ação não foi cancelada.
2) O segundo "segue" do primeiro. Se neste executável passarmos uma referência à propriedade do nosso fragmento, ocorrerá um vazamento de memória, pois a referência ao executável viverá mais que o próprio fragmento.
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, você pode conectar androidx.lifecycle: lifecycle-runtime-ktx: 2.2.0-alpha01 ou superior e já teremos um escopo pronto:
viewLifecycleOwner.lifecycleScope
fun Fragment.delayActionSafe(delayMillis: Long, action: () -> Unit): Job? {
view ?: return null
return viewLifecycleOwner.lifecycleScope.launch {
delay(delayMillis)
action()
}
}
RX
No RX, a classe Disposable é responsável por cancelar as assinaturas, mas no RX não há concorrência estruturada, ao contrário da co-rotina. Por isso, você deve prescrever tudo sozinho. Geralmente é assim:
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()
}
}
Também cancelamos todas as tarefas no fragmento de base da mesma maneira:
class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes),
DisposableHolder by DisposableHolderImpl() {
override fun onDestroyView() {
super.onDestroyView()
dispose()
}
override fun onDestroy() {
super.onDestroy()
dispose()
}
}
E a própria extensão:
fun BaseFragment.delayActionSafe(delayMillis: Long, block: () -> Unit): Disposable? {
view ?: return null
return Completable.timer(delayMillis, TimeUnit.MILLISECONDS).subscribe {
block()
}.also {
addDisposable(it)
}
}
Em custódia
Ao usar qualquer ação adiada, não devemos esquecer que esta já é uma execução assíncrona e, portanto, requer o cancelamento, caso contrário, vazamentos de memória, travamentos e várias outras coisas inesperadas começam a ocorrer.