PreferenceFragmentからDialogFragmentを表示し画面回転で例外発生
こんにちは。
AndroidでPreferenceFragmentからDialogFragmentを表示した時の処理について質問させてください。
まず、Android 2.3にも対応するため、以下のライブラリを使用しております。
android-support-v4
android-support-v7-appcompat
android-support-v4-preferencefragment-master
PreferenceFragmentからDialogFragmentを表示し、DialogFragment内のボタン押下で、PreferenceFragmentに通知するようなプログラムです。
このプログラムでDialogFragmentを表示したまま画面回転を行うと、1回回転させたところでダイアログのOKボタンを押すと、以下の例外で落ちます。
PreferenceFragment内のgetResourcesで落ちていることから、DialogFragmentがコールバックしたPreferenceFragmentのリスナは、フラグメント再生成前の古い物だと考えます。
FATAL EXCEPTION: main
java.lang.IllegalStateException: Fragment PrefFragment{41acf090} not attached to Activity
at android.support.v4.app.Fragment.getResources(Fragment.java:619)
at android.support.v4.app.Fragment.getString(Fragment.java:641)
at com.example.MyActivity$PrefFragment.onOkClicked(MyActivity.java:349)
at com.example.MyDialog$1.onClick(MyDialog.java:149)
at android.view.View.performClick(View.java:4300)
at android.widget.Button.performClick(Button.java:140)
at android.view.View$PerformClick.run(View.java:17570)
at android.os.Handler.handleCallback(Handler.java:725)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5158)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
at dalvik.system.NativeStart.main(Native Method)
さらに、2回回転させると以下の例外で落ちます。
FATAL EXCEPTION: main
java.lang.IllegalStateException: Failure saving state: MyDialog{41844c70 #1 dialog} has target not in fragment manager: PrefFragment{418472d0}
at android.support.v4.app.FragmentManagerImpl.saveAllState(FragmentManager.java:1714)
at android.support.v4.app.FragmentActivity.onSaveInstanceState(FragmentActivity.java:524)
at android.app.Activity.performSaveInstanceState(Activity.java:1147)
at android.app.Instrumentation.callActivityOnSaveInstanceState(Instrumentation.java:1216)
at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3753)
at android.app.ActivityThread.access$700(ActivityThread.java:167)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1282)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5158)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
at dalvik.system.NativeStart.main(Native Method)
setTargetFragment()、getTargetFragment()を使うと、フラグメントの再生成が行われた時でも関連づけが行われるという情報を得て、このプログラムを開発中です。
国内、海外の方々のサイトを見て回ったのですが、同じような事例はあるものの、これといった解決策が見つからず、困っております。
どこに問題があるかおわかりの方、ご教授いただければ幸いです。
MyActivity (PreferenceFragment)
public class MyActivity extends ActionBarActivity {
private static PrefFragment mPreference;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_preference_layout);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
mPreference = new PrefFragment();
transaction.replace(R.id.content_frame, mPreference).commit();
}
public static class PrefFragment extends PreferenceFragment implements MyDialog.OnOkClickListener {
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preference);
// preference.xml内のPreferenceScreenにリスナ登録
screenPrefParent = getText(R.string.key_menu_search_engine_set);
PreferenceScreen prefScreen = (PreferenceScreen) findPreference(screenPrefParent);
prefScreen.setOnPreferenceClickListener(preferenceClickListener);
}
private OnPreferenceClickListener preferenceClickListener = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
// PreferenceScreenがクリックされたのでDialogFragment表示
String engine = (String) preference.getSummary();
MyDialog dialog = MyDialog.newInstance(mPreference, engine); //フラグメントを渡す
FragmentManager manager = getFragmentManager();
dialog.show(manager, "dialog");
return true;
}
};
// DialogFragmentのOKボタンが押された
@Override
public void onOkClicked(Bundle data) {
CharSequence screenPrefParent = getText(R.string.key_menu_search_engine_set);
PreferenceScreen prefScreen = (PreferenceScreen) this.findPreference(screenPrefParent);
String engine = data.getString("ENGINE");
prefScreen.setSummary(engine);
}
};
}
MyDialog (DialogFragment)
public class MyDialog extends DialogFragment {
public interface OnOkClickListener {
public void onOkClicked(Bundle data);
}
public static MyDialog newInstance(Fragment fragment, String engine) {
MyDialog instance = new MyDialog();
instance.setTargetFragment(fragment, 0); // ターゲットフラグメントをここで保存
Bundle bundle = new Bundle(); // 引数を保存
bundle.putString("ENGINE", engine);
instance.setArguments(bundle);
return instance;
}
public MyDialog() {
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = new Dialog(getActivity(), R.style.MyDialogTheme);
dialog.setContentView(R.layout.search_engine_set_dialog);
EditText edit = (EditText) dialog.findViewById(R.id.editSearchEngine);
String text = getArguments().getString("ENGINE");
edit.setText(text);
// OKボタンにリスナ登録
Button button = (Button) dialog.findViewById(R.id.buttonOk);
button.setOnClickListener(okButtonClickListener);
return dialog;
}
private View.OnClickListener okButtonClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Dialog dialog = getDialog();
if (dialog != null) {
try {
// ターゲットフラグメントのリスナを取得
OnOkClickListener listener = (OnOkClickListener) getTargetFragment();
// リスナ呼び出し
Bundle bundle = new Bundle();
EditText edit = (EditText) dialog.findViewById(R.id.editSearchEngine);
bundle.putString("ENGINE", edit.getText().toString());
listener.onOkClicked(bundle);
} catch (ClassCastException e) {
}
}
dismiss();
}
};
}
以上よろしくお願いします。
EDIT 1
自分流に解決策を作ってみたので、書いてみます。
そもそも、今回の事例でsetTargetFragment()、getTargetFragment()を行うのは、DialogFragmentからPreferenceFragmentをコールバックするためでした。
しかし、setTargetFragment()、getTargetFragment()がうまく働いてくれません。setTargetFragment()でフラグメントをセットすると、画面2回回転で
java.lang.IllegalStateException: Failure saving state: MyDialog{41844c70 #1 dialog} has target not in fragment manager: PrefFragment{418472d0}
という例外で落ちてしまう始末。これは、setTargetFragment()されたフラグメントの関連づけを行おうとして、失敗しているものと思われます。
そこで、setTargetFragment()、getTargetFragment()を使わずに、自前のフラグメント管理クラスを作ってみました。
FragmentData (フラグメント1つを格納するクラス)
public class FragmentData {
public String name; // フラグメントの名前
public Fragment fragment; // フラグメント
public FragmentData(String name, Fragment fragment) {
this.name = name;
this.fragment = fragment;
}
}
FragmentDataList (フラグメントを管理するクラス)
class FragmentDataList {
// static なのが重要
private static ArrayList<FragmentData> mFragmentList = new ArrayList<FragmentData>();
// フラグメントを名前付きで記憶する
static void add(String name, Fragment fragment) {
remove(name); // すでに同じ名前の登録があれば消す
FragmentData data = new FragmentData(name, fragment);
mFragmentList.add(data);
}
// 指定された名前のフラグメントの登録を消す
static void remove(String name) {
for (int i = 0; i < mFragmentList.size(); i++ ) {
FragmentData data = mFragmentList.get(i);
if (data.name.equals(name)) {
mFragmentList.remove(i);
break;
}
}
}
// 指定された名前のフラグメントを得る
static Fragment get(String name) {
Fragment ret = null;
for (FragmentData data : mFragmentList) {
if (data.name.equals(name)) {
ret = data.fragment;
break;
}
}
return ret;
}
}
MyActivity (PreferenceFragment)
public class MyActivity extends ActionBarActivity {
// ★★フラグメントに名前をつける
private static final String FRAGMENT_NAME = "SETTING";
private static PrefFragment mPreference;
@Override
public void onCreate(Bundle savedInstanceState) {
...途中省略...
}
public static class PrefFragment extends PreferenceFragment implements MyDialog.OnOkClickListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preference);
// ★★ここでフラグメントを登録する
FragmentDataList.add(FRAGMENT_NAME, this);
...途中省略...
}
private OnPreferenceClickListener preferenceClickListener = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
// PreferenceScreenがクリックされたのでDialogFragment表示
String engine = (String) preference.getSummary();
MyDialog dialog = MyDialog.newInstance(FRAGMENT_NAME, engine); // ★★DialogFragmentにこのフラグメントの名前を渡す
FragmentManager manager = getFragmentManager();
dialog.show(manager, "dialog");
return true;
}
};
@Override
public void onPause() {
super.onPause();
// ★★ここでフラグメントの登録解除
FragmentDataList.remove(FRAGMENT_NAME);
}
@Override
public void onResume() {
super.onResume();
// ★★ここでフラグメントを再登録
FragmentDataList.add(FRAGMENT_NAME, this);
}
};
}
MyDialog (DialogFragment)
public class MyDialog extends DialogFragment {
public interface OnOkClickListener {
public void onOkClicked(Bundle data);
}
public static MyDialog newInstance(String targetFragmentName, String engine) {
MyDialog instance = new MyDialog();
// ★★setTargetFragment()はやめる
// instance.setTargetFragment(fragment, 0); // ターゲットフラグメントをここで保存
Bundle bundle = new Bundle(); // 引数を保存
bundle.putString("TARGET", targetFragmentName); // ★★ターゲットフラグメントの名前を保存
bundle.putString("ENGINE", engine);
instance.setArguments(bundle);
return instance;
}
public MyDialog() {
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
...途中省略...
}
private View.OnClickListener okButtonClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Dialog dialog = getDialog();
if (dialog != null) {
try {
// ターゲットフラグメントのリスナを取得
// ★★getTargetFragment()はやめる
// OnOkClickListener listener = (OnOkClickListener) getTargetFragment();
// ★★ターゲットフラグメントの名前を元に、フラグメントを得てリスナを取得
String targetFragmentName = getArguments().getString("TARGET")
Fragment targetFragment = FragmentDataList.get(targetFragmentName);
OnOkButtonClickListener listener = (OnOkButtonClickListener) targetFragment;
// リスナ呼び出し
Bundle bundle = new Bundle();
EditText edit = (EditText) dialog.findViewById(R.id.editSearchEngine);
bundle.putString("ENGINE", edit.getText().toString());
listener.onOkClicked(bundle);
} catch (ClassCastException e) {
}
}
dismiss();
}
};
}
★★を付けたところが、改変のポイントです。
どうやら、うまく動いているようです。
ただ、PreferenceFragmentからDialogFragmentを表示すると、DialogFragmentを閉じた後、DialogFragmentを開いた回数+1回「戻る」ボタンを押さないと、PreferenceFragmentが終わってくれません。
新たな問題が起こりました。
EDIT 2
ただ、PreferenceFragmentからDialogFragmentを表示すると、DialogFragmentを閉じた後、DialogFragmentを開いた回数+1回「戻る」ボタンを押さないと、PreferenceFragmentが終わってくれません。
新たな問題が起こりました。
と書きましたが、試行錯誤の段階で、DialogFragmentを作成する時に余計な処理を追加してしまっていたのが原因でした。
MyActivity (PreferenceFragment)
...途中省略...
@Override
public boolean onPreferenceClick(Preference preference) {
// ★★ここから
FragmentTransaction ft = getFragmentManager().beginTransaction();
Fragment prev = getFragmentManager().findFragmentByTag("dialog");
if (prev != null) {
ft.remove(prev);
}
ft.addToBackStack(null);
ft.commit();
// ★★ここまでが試行錯誤の段階で追加した部分
// PreferenceScreenがクリックされたのでDialogFragment表示
String engine = (String) preference.getSummary();
MyDialog dialog = MyDialog.newInstance(FRAGMENT_NAME, engine);
FragmentManager manager = getFragmentManager();
dialog.show(manager, "dialog");
上記の★★で囲まれた余計な部分を取り除いたら、「戻る」問題も解決しました。
EDIT 3
onResumeでフラグメントを登録しているので、onCreateでのフラグメントの登録は余分でした。
あと、リストの操作は、きちんと排他制御した方がいいですね。