保存Fragment状态最好的实现

中文翻译 http://www.2cto.com/kf/201503/386389.html


github: https://github.com/nuuneoi/StatedFragment


英文原文地址 http://inthecheesefactory.com/blog/best-approach-to-keep-android-fragment-state/en


ears after struggling with applying the Fragment on Android Application Development, I must say that although Fragment’s concept is brilliant but it comes together with bunch of problems that need to be fixed case by case especially when we need to handle instance state saving.

First of all, although there is an onSaveInstanceState just like the Activity one but it appears that it doesn’t cover all the cases. In the other words, you can’t just rely on onSaveInstanceState to save/restore the view state. Here are the case studies for this story.

Case 1: Rotate screen while there is only 1 fragment in stack

Well, screen rotation is the easiest case to test the instance state saving/restoring. It is easy to handle this case, you just simply save things including member variable which also will be lost from screen rotation in onSaveInstanceState and restore in onActivityCreated or onViewStateRestored just like this:

1
2
3
4
5
6
7
8
9
10
11
12
int someVar;
@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putInt( "someVar" , someVar);
    outState.putString(“text”, tv1.getText().toString());
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super .onActivityCreated(savedInstanceState);
    someVar = savedInstanceState.getInt( "someVar" , 0 );
    tv1.setText(savedInstanceState.getString(“text”));
}

Looks great? Well, not all roses. It appears that there are some case that onSaveInstanceState isn’t called but the View is newly recreated. What does it mean?Everything in the UI is gone.Here is the case.

Case 2: Fragment in Back Stack

 

When fragment is back from backstack (in this case, Fragment A), the view inside Fragment A will be recreated following the Fragment Lifecycle documentedhere.

保存Fragment状态最好的实现_第1张图片

You will see that when fragment returns from backstack, onDestroyView and onCreateView will be called. Anyway, it appears that onSaveInstanceState is not called in this case. The result is everything in the UI is gone and is reset to default as defined in Layout XML.

Anyway, the view that implements inner view state saving, such as EditText or TextView with android:freezeText, still be able to retain the view state since Fragment has implemented state saving for inner view but we developer cannot hook the event. Only way we can do is to manually save instance state in onDestroyView.

1
2
3
4
5
6
7
8
9
10
@Override
public void onSaveInstanceState(Bundle outState) {
    super .onSaveInstanceState(outState);
    // Save State Here
}
@Override
public void onDestroyView() {
    super .onDestroyView();
    // Save State Here
}

Here comes the question, where should we save those instance states to since onDestroyView doesn’t provide any mechanic to save instance state to a Bundle? The answer isan Argumentwhich will still be persisted with Fragment.

The code now looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Bundle savedState;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super .onActivityCreated(savedInstanceState);
    // Restore State Here
    if (!restoreStateFromArguments()) {
       // First Time running, Initialize something here
    }
}
@Override
public void onSaveInstanceState(Bundle outState) {
    super .onSaveInstanceState(outState);
    // Save State Here
    saveStateToArguments();
}
@Override
public void onDestroyView() {
    super .onDestroyView();
    // Save State Here
    saveStateToArguments();
}
private void saveStateToArguments() {
    savedState = saveState();
    if (savedState != null ) {
       Bundle b = getArguments();
       b.putBundle(“internalSavedViewState8954201239547”, savedState);
    }
}
private boolean restoreStateFromArguments() {
    Bundle b = getArguments();
    savedState = b.getBundle(“internalSavedViewState8954201239547”);
    if (savedState != null ) {
       restoreState();
       return true ;
    }
    return false ;
}
/////////////////////////////////
// Restore Instance State Here
/////////////////////////////////
private void restoreState() {
    if (savedState != null ) {
       // For Example
       //tv1.setText(savedState.getString(“text”));
    }
}
//////////////////////////////
// Save Instance State Here
//////////////////////////////
private Bundle saveState() {
    Bundle state = new Bundle();
    // For Example
    //state.putString(“text”, tv1.getText().toString());
    return state;
}

You can now save your fragment's state in saveState and restore it inrestoreState easily. It now looks far better. We are almost there. But there is still another weird case.

Case 3: Rotate screen twice while there is more than one fragment in backstack

When you rotate the screen once, onSaveInstanceState will be called as the UI state will be saved as we expected but when you rotate the screen once more, the above code might crash the application. The reason is although onSaveInstanceState is called but when you rotate the screen, the fragment in the backstack will completely destroy the view and will not create it back until you browse back to the fragment. As a result, the next time you rotate the screen, there is no view to save state.saveState() will crash the application with NullPointerException if you try to access those unexisted view(s).

Here is the workaround, just check that is view existed in fragment. If yes, save it, if not, just pass the savedState saved in Argument and then save it back or just doesn’t do anything since it is already inside the Argument.

1
2
3
4
5
6
7
8
private void saveStateToArguments() {
    if (getView() != null )
       savedState = saveState();
    if (savedState != null ) {
       Bundle b = getArguments();
       b.putBundle(“savedState”, savedState);
    }
}

Yah, it is now done !

Final template for Fragment:

Here comes the fragment template I currently use for my work.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
 
import com.inthecheesefactory.thecheeselibrary.R;
 
/**
  * Created by nuuneoi on 11/16/2014.
  */
public class StatedFragment extends Fragment {
 
     Bundle savedState;
 
     public StatedFragment() {
         super ();
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super .onActivityCreated(savedInstanceState);
         // Restore State Here
         if (!restoreStateFromArguments()) {
             // First Time, Initialize something here
             onFirstTimeLaunched();
         }
     }
 
     protected void onFirstTimeLaunched() {
 
     }
 
     @Override
     public void onSaveInstanceState(Bundle outState) {
         super .onSaveInstanceState(outState);
         // Save State Here
         saveStateToArguments();
     }
 
     @Override
     public void onDestroyView() {
         super .onDestroyView();
         // Save State Here
         saveStateToArguments();
     }
 
     ////////////////////
     // Don't Touch !!
     ////////////////////
 
     private void saveStateToArguments() {
         if (getView() != null )
             savedState = saveState();
         if (savedState != null ) {
             Bundle b = getArguments();
             b.putBundle( "internalSavedViewState8954201239547" , savedState);
         }
     }
 
     ////////////////////
     // Don't Touch !!
     ////////////////////
 
     private boolean restoreStateFromArguments() {
         Bundle b = getArguments();
         savedState = b.getBundle( "internalSavedViewState8954201239547" );
         if (savedState != null ) {
             restoreState();
             return true ;
         }
         return false ;
     }
 
     /////////////////////////////////
     // Restore Instance State Here
     /////////////////////////////////
 
     private void restoreState() {
         if (savedState != null ) {
             // For Example
             //tv1.setText(savedState.getString("text"));
             onRestoreState(savedState);
         }
     }
 
     protected void onRestoreState(Bundle savedInstanceState) {
 
     }
 
     //////////////////////////////
     // Save Instance State Here
     //////////////////////////////
 
     private Bundle saveState() {
         Bundle state = new Bundle();
         // For Example
         //state.putString("text", tv1.getText().toString());
         onSaveState(state);
         return state;
     }
 
     protected void onSaveState(Bundle outState) {
 
     }
}

If you use this template, just simply extends this classextends StatedFragment and save things inonSaveState() and restore them inonRestoreState(). The above code will do the rest for you and I believe that it covers all the possible cases I know.

You might notice that I didn’t setRetainInstance to true which will help developer handling member variable(s) from configuration changed for example, screen rotation. Please note that it is an intention sincesetRetainInstance(true) doesn’t cover all the case. The biggest one is you can’t retain the nested fragment which is being used more frequent time by time so I suggest not to retain instance unless you are 100% sure that the fragment will not be used as nested.

Usage

Good news. StatedFragment described in this blog is now made to be a very easy-to-use library and is already published on jcenter. You can now simply add a dependency like below in your project’s build.gradle.

1
2
3
dependencies {
     compile 'com.inthecheesefactory.thecheeselibrary:stated-fragment-support-v4:0.9.1'
}

Extends StatedFragment and save state in onSaveState(Bundle outState) and restore state inonRestoreState(Bundle savedInstanceState). You are also able to overrideonFirstTimeLaunched(), if you want to do something as the first time the fragment is launched (is not called again after that).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MainFragment extends StatedFragment {
 
     ...
 
     /**
      * Save Fragment's State here
      */
     @Override
     protected void onSaveState(Bundle outState) {
         super .onSaveState(outState);
         // For example:
         //outState.putString("text", tvSample.getText().toString());
     }
 
     /**
      * Restore Fragment's State here
      */
     @Override
     protected void onRestoreState(Bundle savedInstanceState) {
         super .onRestoreState(savedInstanceState);
         // For example:
         //tvSample.setText(savedInstanceState.getString("text"));
     }
 
     ...
 
}

Any comment or suggestion is always welcome !


你可能感兴趣的:(基础框架搭建遇到的问题)