FragmentTransaction.commitAllowingStateLoss

非同期処理とフラグメント操作を組み合わせると少し問題が発生する。
前回の項をベースに、CustomAsyncTask.javaを追加し、

適当な非同期処理として例えば10秒経ったら、MainActivityに定義したreplaceFragmentというメソッドを呼ぶ。
MainActivity.javaで、

package com.bgstation0.android.sample.fragmenttransaction_;

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends Activity {

	// メンバフィールドの定義
	CustomAsyncTask task = null;	// CustomAsyncTaskオブジェクトtaskをnullに.
	Context context = null;	// contextをnullで初期化.
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // contextにthisを格納.
        context = this;
        
        // savedInstanceがnullの時.
        if (savedInstanceState == null){

        	// Buttonの初期化.
            Button button1 = (Button)findViewById(R.id.button1);	// button1を取得.
            button1.setOnClickListener(new OnClickListener() {	// リスナーをセット.
    			
    			@Override
    			public void onClick(View v) {
    				// TODO Auto-generated method stub
    				// CustomAsyncTaskによる非同期処理を生成し, 実行.
    		    	task = new CustomAsyncTask(context);	// CustomAsyncTaskオブジェクトを作成し, taskに格納.
    		    	task.execute(10);	// task.executeに10を渡して実行.
    			}
    			
    		});
            Button button2 = (Button)findViewById(R.id.button2);
            button2.setOnClickListener(new OnClickListener() {	// リスナーをセット.
    			
    			@Override
    			public void onClick(View v) {
    				// TODO Auto-generated method stub
    				
    			}
    			
    		});
            
        }
        
    }
    
    // フラグメントの置換
    public void replaceFragment(){
    	
    	try{
    		FragmentManager fragmentManager = getFragmentManager();	// fragmentManagerの取得.
    		Fragment1 fragment1 = new Fragment1();	// fragment1を生成.
    		FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();	// fragmentTransactionの取得.
    		fragmentTransaction.replace(R.id.framelayout1, fragment1, "fragment1");	// fragment1をreplace.
    		fragmentTransaction.commit();
    		Log.d("MainActivity", "Fragment1");
    		Toast.makeText(context, "replace success!", Toast.LENGTH_LONG).show();
    	}
    	catch(Exception ex){
    		Toast.makeText(context, ex.toString(), Toast.LENGTH_LONG).show();
    		Log.d("MainActivity", ex.toString());
    	}
    }
    
}

Button1を押したら、CustomAsyncTaskが実行され、10秒後に、replaceFragmentによって、フラグメント置換処理が行われる。
これをやってみるのだが、通常は、

Buttonを押して何もせず10秒待つ

Buttonを押して何もせず10秒待つと、

Fragmentが追加され、用意しておいたToastも出る。
Fragmentが追加され、用意しておいたToastも出る。

Fragmentが追加され、用意しておいたToastも出る。
しかし、

ここで10秒待たずに
ここで10秒待たずに

ここで10秒待たずに、

ホームボタンなどで非表示にしてしまうと
ホームボタンなどで非表示にしてしまうと

ホームボタンなどで非表示にしてしまうと、

10秒後にこんなExceptionが出てしまう
10秒後にこんなExceptionが出てしまう

10秒後にこんなExceptionが出てしまう。

ログにもバッチリと出てる
ログにもバッチリと出てる

ログにもバッチリと出てる。
これは、Fragmentを非表示にしたときにonSaveInstanceStateで状態を保存するのだが、その後にFragmentを操作してしまうと、onRestoreInstanceStateで復元する時に保存した状態と操作後の状態が異なってしまい、一貫性がなくなってしまうため、例外を発生させるようになっているため。
そこで、

commitをcommitAllowingStateLossに差し替えると、保存しておいた状態を失ってでも操作を実行するようになる。

FragmentTransaction  |  Android Developers

つまり、

この状態でButton1を押す
この状態でButton1を押す

この状態でButton1を押す。
非同期処理の間に、

ホーム画面にして
ホーム画面にして

ホーム画面にして、

10秒経っても、今度は置換成功。
10秒経っても、今度は置換成功。

10秒経っても、今度は置換成功。

アプリをまた選んで表示状態に戻す
アプリをまた選んで表示状態に戻す

アプリをまた選んで表示状態に戻す。

ちゃんとFragmentは置換されてる
ちゃんとFragmentは置換されてる

ちゃんとFragmentは置換されてる。

Sample/android/FragmentTransaction/commitAllowingStateLoss/src/FragmentTransaction at master · bg1bgst333/Sample · GitHub

FragmentTransaction.commit

FragmentTransaction.commitで、追加や削除などのフラグメント操作処理を確定させる。

FragmentTransaction  |  Android Developers

このcommitが無いと、フラグメントが追加されない。
strings.xmlで、

とし、fragment1_main.xmlで、

TextViewだけ。
fragment2_main.xmlも、

同様。
activity_main.xmlで、

Buttonを2個とFrameLayout。
Fragment1.javaで、

onCreateViewで、レイアウトをinflateしてViewを返すだけ。
Fragment2.javaも、

同様。
MainActivity.javaは、

Button1はaddの後にcommitしない。
Button2はaddの後にcommitする。

起動時
起動時

起動時。
Button1を押しても、

何も起きない
何も起きない

何も起きない。
ログを見ると、

クリックしたときのログしか出ていない
クリックしたときのログしか出ていない

クリックしたときのログしか出ていない。

Button2を押した場合は追加される
Button2を押した場合は追加される

Button2を押した場合は追加される。

ログにFragment側のonCreateViewが出てる
ログにFragment側のonCreateViewが出てる

ログにFragment側のonCreateViewが出てる。

Sample/android/FragmentTransaction/commit/src/FragmentTransaction at master · bg1bgst333/Sample · GitHub

FragmentTransaction.attach

FragmentTransaction.attachは、FragmentTransaction.detachとは逆に、FragmentとViewを再び紐づける。

FragmentTransaction  |  Android Developers

まあ、Viewの再生成だよね。
activity_main.xmlで、

Buttonを3つに増やしている。
(strings.xmlはもう省略。)
MainActivity.javaで、

Button3でattachしている。

起動時
起動時

起動時。

onCreateViewなのでView生成
onCreateViewなのでView生成

onCreateViewなのでView生成。

この時点でButton2押したら、viewはnullではない。
この時点でButton2押したら、viewはnullではない。

この時点でButton2押したら、viewはnullではない。

Button1でdetach
Button1でdetach

Button1でdetach。
TextViewが消えてます。

onDestroyViewだけ
onDestroyViewだけ

onDestroyViewだけ。

Button2を押して、nullになってるのも確認。
Button2を押して、nullになってるのも確認。

Button2を押して、nullになってるのも確認。

Button3を押すと、Fragment1のViewであるTextView"Fragment1"が復活。
Button3を押すと、Fragment1のViewであるTextView"Fragment1"が復活。

Button3を押すと、Fragment1のViewであるTextView"Fragment1"が復活。

またonCreateViewが呼ばれている。
またonCreateViewが呼ばれている。

またonCreateViewが呼ばれている。

Button2でviewがnullでないのも確認した
Button2でviewがnullでないのも確認した

Button2でviewがnullでないのも確認した。

Sample/android/FragmentTransaction/attach/src/FragmentTransaction at master · bg1bgst333/Sample · GitHub

FragmentTransaction.detach

FragmentTransaction.detachは、Fragmentから紐づく"View"を取り外す。

FragmentTransaction  |  Android Developers

Fragment.onDetachは、紐づく"Activity"との関係を解除した時に呼ばれるので、実は無関係。
ただ、今回はFragment.onDetachのコードをベースに、MainActivity.javaを、

こうする。
Button1でFragment1をdetach、Button2であらかじめ保存しておいたFragment1の参照mFragmentのgetViewがnullになるかをチェック。

起動時
起動時

起動時はこう。
この時にButton2を押しても、

viewはnullではない
viewはnullではない

viewはnullではない。
Button1を押すと、

中のTextViewが消えてる
中のTextViewが消えてる

中のTextViewが消えてる。
削除ではなくデタッチされただけ。

なのでonDestroyViewは呼ばれているが、onDestroy、onDetachは呼ばれない。
なのでonDestroyViewは呼ばれているが、onDestroy、onDetachは呼ばれない。

なのでonDestroyViewは呼ばれているが、onDestroy、onDetachは呼ばれない。
Button2を押すと、

viewがnullになってるのがわかる
viewがnullになってるのがわかる

viewがnullになってるのがわかる。

Sample/android/FragmentTransaction/detach/src/FragmentTransaction at master · bg1bgst333/Sample · GitHub

Fragment.onDetach

Fragment.onDetachは、Fragmentとそれに紐づいたActivityの関係が解除された時に呼ばれる。

Fragment  |  Android Developers

removeやreplaceなど、Fragmentが破棄された時に呼ばれる。
onAttachの対となるもの。
Fragment.onDestroyViewのコードをベースに、strings.xmlで、

MainActivity側のButtonを2つにしている。
Fragment1はTextView1つ。
fragment1_main.xmlは、

TextView。
activity_main.xmlは、

Buttonが2つとFrameLayout。
Fragment1.javaは、

onDestroyView、onDestroy、onDetachでgetActivityがnullになるか見ている。
MainActivity.javaは、

Button1がFragment1を削除するのに対し、Button2ではあらかじめ保存しておいたFragment1の参照mFragmentを使ってgetActivityがnullになるかをチェックする。

起動時
起動時

起動時。
Button1でFragment1を削除すると、

こうなる
こうなる

こうなる。
この時、

onDetachでもnullになっているわけではない
onDetachでもnullになっているわけではない

onDetachでもnullになっているわけではない。
実は、onDetachは解除する直前に呼ばれるので、nullは反映されてない。
そこでButton2を改めて押すと、

MainActivityのButton2でチェックしたgetActivityはnullになっている。
MainActivityのButton2でチェックしたgetActivityはnullになっている。

MainActivityのButton2でチェックしたgetActivityはnullになっている。

Sample/android/Fragment/onDetach/src/Fragment at master · bg1bgst333/Sample · GitHub

FragmentManager.getFragment

FragmentManager.getFragmentで、保存しておいたFragmentの参照を取得する。

FragmentManager  |  Android Developers

改めて、今挿入しているFragmentをmCurrentFragmentとして参照を持ち、それを操作するような場合、どのような形がいいかを検証していく。
まず、MainActivity.javaを、

package com.bgstation0.android.sample.fragmentmanager_;

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

//メインアクティビティ
public class MainActivity extends Activity implements OnClickListener{

	// 生成時
    @Override
    protected void onCreate(Bundle savedInstanceState) {
 	
 	    // 既定の処理.
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
     
        // Button1の処理.
        Button button1 = (Button)findViewById(R.id.button1);	// button1を取得.
        button1.setOnClickListener(this);	// リスナーとしてthisをセット.
     
    }
    
    // クリック時
    public void onClick(View v){
 	
        // FragmentManager, fragmentTransactionの取得.
    	FragmentManager fragmentManager = getFragmentManager();	// fragmentManagerの取得.
    	FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();	// fragmentTransactionの取得.
    	Fragment1 fragment1 = new Fragment1();	// fragment1を生成.
    	fragmentTransaction.add(R.id.framelayout1, fragment1);	// fragment1を追加.
    	fragmentTransaction.commit();	// コミット.
 	
    }
    
}

こうする。
まだ、mCurrentFragmentを用意してない。
Buttonを押したらFragmentを追加する状況。

起動時
起動時

起動時。
Button1を押すと、

Fragment追加される
Fragment追加される

Fragment追加される。
FButton1を押すと、

数字が増えて
数字が増えて

数字が増えて、横にすると、

Fragmentは再生成されるが0
Fragmentは再生成されるが0

Fragmentは再生成されるが0。
さて、ここにmCurrentFragmentを入れてみる。

package com.bgstation0.android.sample.fragmentmanager_;

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

//メインアクティビティ
public class MainActivity extends Activity implements OnClickListener{

	// メンバフィールド.
	Fragment mCurrentFragment = null;	// mCurrentFragmentをnullで初期化.
	
	// 生成時
    @Override
    protected void onCreate(Bundle savedInstanceState) {
 	
 	    // 既定の処理.
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
     
        // Button1の処理.
        Button button1 = (Button)findViewById(R.id.button1);	// button1を取得.
        button1.setOnClickListener(this);	// リスナーとしてthisをセット.
        
        // mCurrentFragmentのチェック.
        if (mCurrentFragment == null){
        	Log.d("MainActivity", "onCreate:mCurrentFragment == null");
        }
        else{
        	Log.d("MainActivity", "onCreate:mCurrentFragment = " + mCurrentFragment.toString());
        }
     
    }
    
    // クリック時
    public void onClick(View v){
 	
        // FragmentManager, fragmentTransactionの取得.
    	FragmentManager fragmentManager = getFragmentManager();	// fragmentManagerの取得.
    	FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();	// fragmentTransactionの取得.
    	Fragment1 fragment1 = new Fragment1();	// fragment1を生成.
    	fragmentTransaction.add(R.id.framelayout1, fragment1);	// fragment1を追加.
    	fragmentTransaction.commit();	// コミット.
    	mCurrentFragment = fragment1;	// mCurrentFragmentにfragment1をセット.
    	Log.d("MainActivity", "onClick:mCurrentFragment = " + mCurrentFragment.toString());
    	
    }
    
}

追加をしたときにmCurrentFragmentにfragment1を格納する。

起動時
起動時

起動時は、

mCurrentFragmentはnull
mCurrentFragmentはnull

mCurrentFragmentはnull。

Button1で追加されたら
Button1で追加されたら

Button1で追加されたら、

中に入ってる
中に入ってる

中に入ってる。
ただし、

横にして再生成されると
横にして再生成されると

横にして再生成されると、

Activityも再生成されるので、またnullになってしまう。
Activityも再生成されるので、またnullになってしまう。

Activityも再生成されるので、またnullになってしまう。
ここで2つのことを実現したい。
1つ目は、起動時にFragmentが追加されていること。
2つ目は、回転してもFragmentの参照が格納されていること。

1つ目は、

package com.bgstation0.android.sample.fragmentmanager_;

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

//メインアクティビティ
public class MainActivity extends Activity implements OnClickListener{

	// メンバフィールド.
	Fragment mCurrentFragment = null;	// mCurrentFragmentをnullで初期化.
	
	// 生成時
    @Override
    protected void onCreate(Bundle savedInstanceState) {
 	
 	    // 既定の処理.
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
     
        // Button1の処理.
        Button button1 = (Button)findViewById(R.id.button1);	// button1を取得.
        button1.setOnClickListener(this);	// リスナーとしてthisをセット.
        
        // savedInstanceStateのチェック.
        if (savedInstanceState == null){	// nullの時.
        	
        	// FragmentManager, fragmentTransactionの取得.
        	FragmentManager fragmentManager = getFragmentManager();	// fragmentManagerの取得.
        	FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();	// fragmentTransactionの取得.
        	Fragment1 fragment1 = new Fragment1();	// fragment1を生成.
        	fragmentTransaction.add(R.id.framelayout1, fragment1);	// fragment1を追加.
        	fragmentTransaction.commit();	// コミット.
        	mCurrentFragment = fragment1;	// mCurrentFragmentにfragment1をセット.
        	
        }
        
        // mCurrentFragmentのチェック.
        if (mCurrentFragment == null){
        	Log.d("MainActivity", "onCreate:mCurrentFragment == null");
        }
        else{
        	Log.d("MainActivity", "onCreate:mCurrentFragment = " + mCurrentFragment.toString());
        }
     
    }
    
    // クリック時
    public void onClick(View v){
 	
        // FragmentManager, fragmentTransactionの取得.
    	//FragmentManager fragmentManager = getFragmentManager();	// fragmentManagerの取得.
    	//FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();	// fragmentTransactionの取得.
    	//Fragment1 fragment1 = new Fragment1();	// fragment1を生成.
    	//fragmentTransaction.add(R.id.framelayout1, fragment1);	// fragment1を追加.
    	//fragmentTransaction.commit();	// コミット.
    	//mCurrentFragment = fragment1;	// mCurrentFragmentにfragment1をセット.
    	//Log.d("MainActivity", "onClick:mCurrentFragment = " + mCurrentFragment.toString());
    	
    }
    
}

これまでも出てきたonCreate時のsavedInstanceStateのnullチェック分岐で実現する。
その代わり、Buttonはもう実質使わない。

起動時に既に追加されている
起動時に既に追加されている

起動時に既に追加されている。
この時はsavedInstanceStateはnullだからね。

onCreateで追加されてる
onCreateで追加されてる

onCreateで追加されてる。

横にした場合は、onCreateのsavedInstanceStateはnullではないので、既に追加されてる以上、追加処理をする必要はない
横にした場合は、onCreateのsavedInstanceStateはnullではないので、既に追加されてる以上、追加処理をする必要はない

横にした場合は、onCreateのsavedInstanceStateはnullではないので、既に追加されてる以上、追加処理をする必要はない。
ただし、

そうすると、mCurrentFragmentはnullになってしまう。
そうすると、mCurrentFragmentはnullになってしまう。

そうすると、mCurrentFragmentはnullになってしまう。
そこで、onSaveInstanceState/onRestoreInstanceState、そしてputFragment/getFragmentが必要になってくる。

こんな風にすることで、

起動時
起動時

起動時、

しっかり追加されている。
しっかり追加されている。

しっかり追加されている。

回転しても
回転しても

回転しても、

復元されている
復元されている

復元されている。
もちろん、永続化ではないので、インスタンスが同じものというわけではない。

Sample/android/FragmentManager/getFragment/src/FragmentManager at master · bg1bgst333/Sample · GitHub