blog

デイリーピットクロール - フラグメントパスの失敗

bの属性が取得できず、NULLポインタ例外が発生するケースがありました。 フラグメントの作成のロジックをチェックし、元のフラグメントのプロパティが新しく作成されたフラグメントメンに割り当てられないこと...

Jan 16, 2021 · 5 min. read
シェア

最近buglyで、フラグメントにパラメータを渡す際にアクティビティが代入を使っている、以下のような非常に奇妙な問題を見かけました:

val f = FragmentA()
f.b = "sss"

場合によっては、bの属性が水面下で死んでしまい、プログラムがヌル・ポインタ例外を投げてしまいます。

原因分析

androidx バージョン: 1.1.0

FragmentActivityのコードを見てみました。

protected void onCreate(@Nullable Bundle savedInstanceState) {
 mFragments.attachHost(null /*parent*/);
 if (savedInstanceState != null) {
 Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
 mFragments.restoreSaveState(p);
 ......
 }
 .....
}
 Fragment f = fs.instantiate(mHost.getContext().getClassLoader(), getFragmentFactory());

フラグメントの作成のロジックを表示すると、元のフラグメントのプロパティは、新しく作成されたフラグメントメンに割り当てられないことがわかり、フラグメントの唯一のリカバリデータは、なぜsetArgumentsを使用する公式の推奨は、パラメータの理由を渡します。

public Fragment instantiate(@NonNull ClassLoader classLoader,
 @NonNull FragmentFactory factory) {
 if (mInstance == null) {
 if (mArguments != null) {
 mArguments.setClassLoader(classLoader);
 }
 mInstance = factory.instantiate(classLoader, mClassName);
 mInstance.setArguments(mArguments); // fragmentデータを復元する唯一の操作
 if (mSavedFragmentState != null) {
 mSavedFragmentState.setClassLoader(classLoader);
 mInstance.mSavedFragmentState = mSavedFragmentState;
 } else {
 // When restoring a Fragment, always ensure we have a
 // non-null Bundle so that developers have a signal for
 // when the Fragment is being restored
 mInstance.mSavedFragmentState = new Bundle();
 }
 mInstance.mWho = mWho;
 mInstance.mFromLayout = mFromLayout;
 mInstance.mRestored = true;
 mInstance.mFragmentId = mFragmentId;
 mInstance.mContainerId = mContainerId;
 mInstance.mTag = mTag;
 mInstance.mRetainInstance = mRetainInstance;
 mInstance.mRemoving = mRemoving;
 mInstance.mDetached = mDetached;
 mInstance.mHidden = mHidden;
 mInstance.mMaxState = Lifecycle.State.values()[mMaxLifecycleState];
 if (FragmentManagerImpl.DEBUG) {
 Log.v(FragmentManagerImpl.TAG, "Instantiated fragment " + mInstance);
 }
 }
 return mInstance;
 }

androidx 1.2.4

FragmentManagerViewModelリストアする場合:まず、.NET Frameworkのフラグメントのインスタンスがあるかどうかを調べます。

インスタンスが存在する場合、FragmentStateが復元され、その場合、フラグメントのプロパティはまだ存在します。

インスタンスがない場合は、リフレクションによってフラグメントが再作成され、新しいフラグメントに保存されたmArgumentsが与えられます。

void restoreSaveState(@Nullable Parcelable state) {
 // If there is no saved state at all, then there's nothing else to do
 if (state == null) return;
 FragmentManagerState fms = (FragmentManagerState) state;
 if (fms.mActive == null) return;
 // Build the full list of active fragments, instantiating them from
 // their saved state.
 mFragmentStore.resetActiveFragments();
 for (FragmentState fs : fms.mActive) {
 if (fs != null) {
 FragmentStateManager fragmentStateManager;
 Fragment retainedFragment = mNonConfig.findRetainedFragmentByWho(fs.mWho);
 if (retainedFragment != null) {
 if (isLoggingEnabled(Log.VERBOSE)) {
 Log.v(TAG, "restoreSaveState: re-attaching retained "
 + retainedFragment);
 }
 // 
 fragmentStateManager = new FragmentStateManager(mLifecycleCallbacksDispatcher,
 retainedFragment, fs);
 } else {
 // フラグメントをここに再作成する
 fragmentStateManager = new FragmentStateManager(mLifecycleCallbacksDispatcher,
 mHost.getContext().getClassLoader(), getFragmentFactory(), fs);
 }
 Fragment f = fragmentStateManager.getFragment();
 f.mFragmentManager = this;
 if (isLoggingEnabled(Log.VERBOSE)) {
 Log.v(TAG, "restoreSaveState: active (" + f.mWho + "): " + f);
 }
 fragmentStateManager.restoreState(mHost.getContext().getClassLoader());
 mFragmentStore.makeActive(fragmentStateManager);
 // Catch the FragmentStateManager up to our current state
 // In almost all cases, this is Fragment.INITIALIZING, but just in
 // case a FragmentController does something...unique, let's do this anyways.
 fragmentStateManager.setFragmentManagerState(mCurState);
 }
 }
 ........
 }

フラグメントへの引数の割り当て

 FragmentStateManager(@NonNull FragmentLifecycleCallbacksDispatcher dispatcher,
 @NonNull ClassLoader classLoader, @NonNull FragmentFactory fragmentFactory,
 @NonNull FragmentState fs) {
 mDispatcher = dispatcher;
 	// リフレクションを使ってフラグメントを再作成する
 mFragment = fragmentFactory.instantiate(classLoader, fs.mClassName);
 if (fs.mArguments != null) {
 fs.mArguments.setClassLoader(classLoader);
 }
 mFragment.setArguments(fs.mArguments);
 	.....
 }

解決策

その原因を知ることで、それを解決することができます。

val f = FragmentA()
f.arguments = Bundle().apply { 
 putString("sk", "bbbb")
}

kotlinでコードを最適化

拡張関数 + 使用は、よりエレガントです。

fun <T> Fragment.putArgument(
 key: String,
 value: T
) {
 if (this.arguments == null) {
 this.arguments = Bundle()
 }
 when (value) {
 is Int -> this.requireArguments().putInt(key, value)
 is Boolean -> this.requireArguments().putBoolean(key, value)
 is String -> this.requireArguments().putString(key, value)
 is CharSequence -> this.requireArguments().putCharSequence(key, value)
 is Float -> this.requireArguments().putFloat(key, value)
 is Long -> this.requireArguments().putLong(key, value)
 is Bundle -> this.requireArguments().putBundle(key, value)
 is Serializable -> this.requireArguments().putSerializable(key, value)
 is Parcelable -> this.requireArguments().putParcelable(key, value)
 is Char -> this.requireArguments().putChar(key, value)
 is Byte -> this.requireArguments().putByte(key, value)
 else -> error("サポートされていない型, $value")
 }
}
fun <T> Fragment.getArgument(key: String): T {
 return this.arguments?.get(key) as T
}

使う

// データを設定する
class ActivityA :Activity{
 fun startFragment(){
 val f = FragmentA()
 f.putArgument("book","リトル・イエロー・ブック")
 }
}
class FragmentA: Fragment{
 val bk: String by lazy {getArgument<String>("book")} // 遅延ロードを使用して、データが一度しか取得されないようにする
}
Read next

幸福感を高めるいくつかの方法

最近、233ソースは実は忙しい。3ヶ月以上続いたプロジェクトの開発・再構築で、毎日一緒に残業したことのないパートナーに引っ張られて1095~10105くらいの残業リズム。彼は最近、基本的に私たちの会社で最も頻繁に残業で2年以上であると述べました。 それは最終的に今週オンラインになる予定でしたが、問題のnetty非同期コールバック不適切な使用のため、それは別の2日間かかった...

Jan 15, 2021 · 1 min read