Fragmentを使った実装をしているときにViewを参照しようとして画面がクラッシュするケースがあります。
例えば
- 親のActivityからFragmentを参照したが、Fragmentのライフサイクルは
Destroyed
だった - なぜか意図しないタイミングでコールされる
OnScrollChangedListener
-
onActivityCreated
やonResume
のタイミングでセットしたRunnable
処理
などです。
なぜかonDestroyView
やonPause
でリスナーを切っていてもリリース後に数件とかちらほら上がってくることがあります。
画面が破棄されたかどうかのチェックがしづらくなった
これはLiveData
やCoroutine
がKTXなどの拡張モジュールも充実してきていて、ライフサイクルに紐づいた処理ができるのでプロダクトで使えていればメモリーリークや意図しないViewの参照によるクラッシュが起こりづらくなってきたためだと思われます。とはいえ、歴史のあるプロダクトでは古いコードが乱立している中、全てのサービスでトレンドに沿った実装ができるわけではないと思います。
かといって、DataBinding
がnull
かどうかのチェックは現実的ではないし、lateinit
も画面破棄後のチェックはしづらいです。
ButterKnifeが全盛だった頃は、Fragment内でUnbinder
クラスをNullable
で定義していたので、画面が破棄されたかどうかはUnbinderがnullかどうか
で判別していたりしました。
isViewDestroyedフラグを作る
各リスナーのコールバックの中で下記のような早期リターンを付けてあげるようにしました。
Fragmentクラスの中に良さげな変数を見つけたのでそれを使ってみます。
fun Fragment.isViewDestroyed(): Boolean {
return viewLifecycleOwnerLiveData.value == null
}
viewLifecycleOwnerLiveData
はLifecycleOwner
をLiveDataで管理していて、onCreateViewからonDestroyViewのスコープでvalueを取得できるみたいです。逆にonDestroyView以降の参照であればvalueはnull
になります。
/**
* Get a {@link LifecycleOwner} that represents the {@link #getView() Fragment's View}
* lifecycle. In most cases, this mirrors the lifecycle of the Fragment itself, but in cases
* of {@link FragmentTransaction#detach(Fragment) detached} Fragments, the lifecycle of the
* Fragment can be considerably longer than the lifecycle of the View itself.
* <p>
* Namely, the lifecycle of the Fragment's View is:
* <ol>
* <li>{@link Lifecycle.Event#ON_CREATE created} after {@link #onViewStateRestored(Bundle)}</li>
* <li>{@link Lifecycle.Event#ON_START started} after {@link #onStart()}</li>
* <li>{@link Lifecycle.Event#ON_RESUME resumed} after {@link #onResume()}</li>
* <li>{@link Lifecycle.Event#ON_PAUSE paused} before {@link #onPause()}</li>
* <li>{@link Lifecycle.Event#ON_STOP stopped} before {@link #onStop()}</li>
* <li>{@link Lifecycle.Event#ON_DESTROY destroyed} before {@link #onDestroyView()}</li>
* </ol>
*
* The first method where it is safe to access the view lifecycle is
* {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} under the condition that you must
* return a non-null view (an IllegalStateException will be thrown if you access the view
* lifecycle but don't return a non-null view).
* <p>The view lifecycle remains valid through the call to {@link #onDestroyView()}, after which
* {@link #getView()} will return null, the view lifecycle will be destroyed, and this method
* will throw an IllegalStateException. Consider using
* {@link #getViewLifecycleOwnerLiveData()} or {@link FragmentTransaction#runOnCommit(Runnable)}
* to receive a callback for when the Fragment's view lifecycle is available.
* <p>
* This should only be called on the main thread.
* <p>
* Overriding this method is no longer supported and this method will be made
* <code>final</code> in a future version of Fragment.
*
* @return A {@link LifecycleOwner} that represents the {@link #getView() Fragment's View}
* lifecycle.
* @throws IllegalStateException if the {@link #getView() Fragment's View is null}.
*/
@MainThread
@NonNull
public LifecycleOwner getViewLifecycleOwner() {
if (mViewLifecycleOwner == null) {
throw new IllegalStateException("Can't access the Fragment View's LifecycleOwner when "
+ "getView() is null i.e., before onCreateView() or after onDestroyView()");
}
return mViewLifecycleOwner;
}
/**
* Retrieve a {@link LiveData} which allows you to observe the
* {@link #getViewLifecycleOwner() lifecycle of the Fragment's View}.
* <p>
* This will be set to the new {@link LifecycleOwner} after {@link #onCreateView} returns a
* non-null View and will set to null after {@link #onDestroyView()}.
* <p>
* Overriding this method is no longer supported and this method will be made
* <code>final</code> in a future version of Fragment.
*
* @return A LiveData that changes in sync with {@link #getViewLifecycleOwner()}.
*/
@NonNull
public LiveData<LifecycleOwner> getViewLifecycleOwnerLiveData() {
return mViewLifecycleOwnerLiveData;
}
getViewLifecycleOwner
はonDestroyViewが呼ばれた後に参照するとエラーになるのでviewLifecycleOwnerLiveData
を見る方が良さそうです。
Overriding this method is no longer supported and this method will be made <code>final</code> in a future version of Fragment.
この一文が気になるけど
Link
- Suspending over Views