LoginSignup
1
0

More than 3 years have passed since last update.

DataBinding/ViewBindingはNonNullかlateinit管理なので意図しないViewへの参照をどうやって防ぐのが良いか

Last updated at Posted at 2019-12-11

Fragmentを使った実装をしているときにViewを参照しようとして画面がクラッシュするケースがあります。

例えば

  • 親のActivityからFragmentを参照したが、FragmentのライフサイクルはDestroyedだった
  • なぜか意図しないタイミングでコールされるOnScrollChangedListener
  • onActivityCreatedonResumeのタイミングでセットしたRunnable処理

などです。
なぜかonDestroyViewonPauseでリスナーを切っていてもリリース後に数件とかちらほら上がってくることがあります。

画面が破棄されたかどうかのチェックがしづらくなった

これはLiveDataCoroutineがKTXなどの拡張モジュールも充実してきていて、ライフサイクルに紐づいた処理ができるのでプロダクトで使えていればメモリーリークや意図しないViewの参照によるクラッシュが起こりづらくなってきたためだと思われます。とはいえ、歴史のあるプロダクトでは古いコードが乱立している中、全てのサービスでトレンドに沿った実装ができるわけではないと思います。

かといって、DataBindingnullかどうかのチェックは現実的ではないし、lateinitも画面破棄後のチェックはしづらいです。
ButterKnifeが全盛だった頃は、Fragment内でUnbinderクラスをNullableで定義していたので、画面が破棄されたかどうかはUnbinderがnullかどうかで判別していたりしました。

isViewDestroyedフラグを作る

各リスナーのコールバックの中で下記のような早期リターンを付けてあげるようにしました。
Fragmentクラスの中に良さげな変数を見つけたのでそれを使ってみます。

FragmentExt.kt
fun Fragment.isViewDestroyed(): Boolean {
    return viewLifecycleOwnerLiveData.value == null
}

viewLifecycleOwnerLiveDataLifecycleOwnerをLiveDataで管理していて、onCreateViewからonDestroyViewのスコープでvalueを取得できるみたいです。逆にonDestroyView以降の参照であればvalueはnullになります。

Fragment.java
/**
     * 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.

この一文が気になるけど:innocent:

Link

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0