中文翻译 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.
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 !
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.
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 !