最近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")} // 遅延ロードを使用して、データが一度しか取得されないようにする
}





