Handling fragments(lifecycle) in android app was complicated from the beginning. Nowadays, a lot of internal bugs is fixed, but still there is a general negativity towards fragment usage.
Many developers are avoiding fragments and returning to old fashion apps with the large number of the activities. Just because it’s “complicated”, it does not mean that we have to stop using it. Fragments are (at the moment) part of our apps and we have to deal with it the best we can.
These issues might feel a little bit basic, however, by working on many android apps, I’ve noticed few constant problem-patterns (listed-below) when it comes to fragments. If you’d just like to jump to implementation, click here.
Getting instance of Fragment (for whatever reason) from the existing stack of transactions, regardless of the position in the stack.
One way of getting the last added fragment might be this one:
1 2 3 4 5
//fetching all fragments List<Fragment> fragments = fragmentManager.getFragments(); //getting last added fragment Fragment currentFragment = fragments.get(fragments.size() - 1);
Although this approach makes sense, it is possible that some fragments are removed in the meantime and this list will contain
null
(instead of trimmed size).If, for example, we have
three
fragments on stack and we just tapped back button, we might getfragment.size() == 3
but actual number of fragments istwo
and third positionfragments.get(fragments.size() - 1)
will returnnull
.And I cannot point the obvious more than it is, but this is the stack and still it can happen with any other index in the list. Since we have a
List<>
interface exposed, then we can callfragments.get(0)
and wow, it returnsnull
,fragments.size()
value will be incorrect and no matter what index we use, we won’t get correct fragment instance.Then we might say, ok we can use this approach instead:
1 2 3 4 5 6 7 8
//fetching all fragments List<Fragment> fragments = fragmentManager.getFragments(); //getting the actual number of fragments from the fragmentManager int fragmentsCount = fragmentManager.getBackStackEntryCount(); //getting last added fragment Fragment currentFragment = fragments.get(fragmentsCount - 1);
And great, now we have the exact number of fragments on the stack, however,
fragments
list still can containnull
values on any position and we’ll end up with wrong fragment or withjava.lang.IndexOutOfBoundsException
in the worst case. Additional work is needed anyway and it seams its a lot of work just to get instance of fragment we have created in the first place.Handling onBackPressed navigation and application title update
Typical android application shows
Toolbar
on top of the screen. If theToolbar
is part of theActivity
layout we need to update itstitle
according to currently active fragment.For example:
HomeActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
@Inject FragmentManager supportFragmentManager; @BindView(R.id.toolbar) Toolbar toolbar; public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); ... setSupportActionBar(toolbar); supportFragmentManager.beginTransaction() .add(R.id.activity_home_fragment_container, HomeFragment.newInstance()) .addToBackStack(null) .commit(); } public void setTitle(@StringRes int titleId){ getSupportActionBar().setTitle(getString(titleId)); }
HomeFragment.java
1 2 3 4 5
@Override public void onStart() { super.onStart(); ((HomeActivity)getActivity()).setTitle(R.string.home); }
And this actually works. Except you might get
NullPointerException
sometimes orjava.lang.IllegalStateException: Fragment LoginFragment not attached to Activity
and honestly, I’ve stopped looking for reasons why. If you movegetActivity().setTitle(R.string.login);
to some other location in the fragment,setTitle
might get called too late and we’re triggering bad user experience.On the other hand, when you’re popping the fragment from the stack, you have to call
setTitle()
method of theFragment
which is popping up and at some pointsetTitle()
method might not get called at all, and you end up withtitle
in theToolbar
that does not match the content.It does not have to be the title, you might want to change navigation options for different fragments, to show back arrow instead of hamburger icon etc, and setting these options will depend of some method call inside the
Fragment
.Reseting stack triggers lifecycle of each fragment along the way
At some point, we have to return to the starting point of the app. So we’ll be popping the fragments from the stack until stack gets empty or until there is only one fragment shown. Not so obvious problem that occurs is that each popped fragment gets re-instantiated with call to all lifecycle methods, and causing execution of logic we put in each fragment. This is definitely unwanted behavior. We just want to remove them so that we can start from the beginning.
Communication between fragments and the hosting Activity should not be implemented by casting
getActivity()
method and I like to think that no one implements it like that. Instead, communication between activity and the fragments should be implemented via interface whichActivity
implements, I know, obvious.
Simple solution I came up with have four main components:
SimpleFragmentManager
which is basically a wrapper aroundFragmentManager
and exposes the methods foradd
,replace
,popUp
,popUpAll
andgetCurrentFragment
operations.SimpleFragmentManager
assumes that eachFragment
implementsIFragment
interface.IFragment
interface is forcing eachFragment
to have its own uniquename
ortag
under which is added to transaction,dispose()
method (which is very handy when implementingRxJava2
), and yes, method tosetTitle()
of theToolbar/ActionBar
.FragmentTagStack
to handle stack easily and retrieve fragments. It does not contain fragment instances, but itstags
asString
values instead and have only basic methods forpop
,popUpAll
,peek
,push
andgetActiveTag
which returns tag of currently shownFragment
on the screen.FragmentChannel
is the most basic interface for callback implementation between theFragment
and hosting environment, which can beActivity
or parentFragment
.
And here is the sequence diagram that simplifies the explanation, that shows basic interaction with adding, replacing and removing fragments.
It is obvious that implementation is really simple. To make things even more easier, sources can be easily found by referencing the library in your build.gradle
file: implementation 'com.bajicdusko:fragment-manager:1.0.2'
.
You have the freedom to reimplement base classes for the activities and fragments, or you can extend already prepared SFMActivity
and SFMFragment
class which do all the work for you.
In case that you’re unable to extend these classes, these are the preconditions you have to prepare in your own class.
- Activity initialize
SimpleFragmentManager
.- Override
onSaveInstanceState
andonRestoreInstanceState
methods and forward the state toSimpleFragmentManager
. - Override
onBackPressed
method forward the call toSimpleFragmentManager
. - Implement
FragmentChannel
.
- Override
- Each
Fragment
implementsIFragment
interface. Tip: Usually, havingabstract BaseFragment
class which implementsIFragment
and initializesFragmentChannel
is less error prone, then implementing these methods for all Fragments individually.
Example implementation can be found in this sample project. as well as implementations of described components below. Once you establish hierarchy as below, you won’t have to think about it much. Only component that you’ll update constantly is FragmentChannel
interface since you’ll be implementing all Fragment
-> Activity
calls through it. showLogin()
function in example below is one such case.
SimpleFragmentManager.kt
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
class SimpleFragmentManager(private val fragmentManager: FragmentManager, private val fragmentContainerId: Int) {
private val KEY_TAGS = "key_tags"
private var fragmentTagStack: FragmentTagStack = FragmentTagStack()
constructor(fragmentManager: FragmentManager, fragmentContainer: FrameLayout) : this(fragmentManager, fragmentContainer.id)
fun addFragment(fragment: IFragment) {
fragmentTagStack.push(fragment.getFragmentName())
fragmentManager.beginTransaction()
.add(fragmentContainerId, fragment as Fragment, fragment.getFragmentName())
.addToBackStack(fragment.getFragmentName())
.commit()
}
fun replaceFragment(fragment: IFragment) {
fragmentTagStack.push(fragment.getFragmentName())
fragmentManager.beginTransaction()
.replace(fragmentContainerId, fragment as Fragment, fragment.getFragmentName())
.addToBackStack(fragment.getFragmentName())
.commit()
}
/**
* When [Activity.onBackPressed] is called it is a good practice to override it and to call this method.
* This method won't allow removal of last fragment on the stack.
*
* @return TRUE if fragment is poppedUp aka method is consumed, or FALSE if there is only one Fragment
* on the stack.
*/
fun onBackPressed(): Boolean {
if (fragmentManager.backStackEntryCount > 1) {
popUp()
var currentFragment = getCurrentFragment()
currentFragment?.setTitle()
return true
} else {
return false
}
}
fun popUp() {
fragmentManager.popBackStackImmediate()
fragmentTagStack.pop()
}
fun popUpAll() {
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
fragmentTagStack.popUpAll()
}
fun getCurrentFragment(): IFragment? = fragmentManager.findFragmentByTag(fragmentTagStack.activeTag) as IFragment?
fun dispose() = getCurrentFragment()?.dispose()
fun onSaveInstanceState(state: Bundle?) {
state?.putParcelable(KEY_TAGS, Parcels.wrap(fragmentTagStack))
}
fun onRestoreInstanceState(savedInstanceState: Bundle?) {
if (savedInstanceState != null) {
fragmentTagStack = Parcels.unwrap(savedInstanceState.getParcelable(KEY_TAGS))
} else {
fragmentTagStack = FragmentTagStack()
}
}
}
FragmentTagStack.kt
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
@Parcel(Parcel.Serialization.BEAN)
class FragmentTagStack {
@Transient private var showLogs: Boolean = false
var tags: MutableList<String>
private set
var activeTag: String? = null
private set
constructor(){
tags = LinkedList()
}
@ParcelConstructor
constructor(tags: MutableList<String>, activeTag: String) {
this.tags = tags
this.activeTag = activeTag
}
/**
* Enabling or disabling fragment stack logs. Logs are disabled by default.
* @param showLogs
*/
fun setShowLogs(showLogs: Boolean) {
this.showLogs = showLogs
}
/**
* Pushing new tag to stack and setting is as [FragmentTagStack.activeTag]
* @param tag
*/
fun push(tag: String) {
tags.add(tag)
activeTag = tag
logStack()
}
/**
* Popping tag from the stack and setting the tag below as new [FragmentTagStack.activeTag]
*
*
* Example:
*
*
* Stack [3, 2, 1, 0] (3 is last added tag). [FragmentTagStack.activeTag] have value 3
*
*
* Now we're popping the last added tag
*
*
* Stack [2, 1, 0] [FragmentTagStack.activeTag] have value 2
* @return Popped up value. From the example above, returned value will be 3. In case of empty stack, null will be returned.
*/
fun pop(): String? {
val tag = peek()
if (!TextUtils.isEmpty(tag)) {
tags.removeAt(tags.size - 1)
}
activeTag = peek()
logStack()
return tag
}
/**
* Peeking to the top of the stack, without data modification.
* @return Value on top of the stack. If stack is empty, returned value will be null.
*/
fun peek(): String? {
if (tags.size > 0) {
return tags[tags.size - 1]
} else {
return null
}
}
private fun logStack() {
if (showLogs) {
val stringBuilder = StringBuilder()
stringBuilder.append("Active tag: $activeTag\nStack:\n")
for (i in tags.size - 1 downTo 0) {
stringBuilder.append("[ ${tags[i]}]\n")
}
Log.d(TAG, stringBuilder.toString())
}
}
/**
* Clears the stack.
*/
fun popUpAll() {
tags.clear()
}
companion object {
@Transient private val TAG = "FragmentTagStack"
}
}
IFragment.kt
1
2
3
4
5
6
7
8
interface IFragment {
fun getFragmentName(): String
fun setTitle(): Unit?
fun dispose()
}
FragmentChannel.kt
1
2
3
4
5
interface FragmentChannel{
fun setTitle(title: String): Unit?
fun showLogin()
}
BaseFragment.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class BaseFragment : Fragment(), IFragment{
var fragmentChannel: FragmentChannel? = null
override fun onAttach(context: Context){
super.onAttach(context)
if(context is FragmentChannel){
fragmentChannel = context
}
}
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
if(parentFragment != null && parentFragment is FragmentChannel){
fragmentChannel = parentFragment
}
}
}
HomeFragment.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HomeFragment : BaseFragment() {
private val FRAGMENT_NAME = "Home"
companion object{
fun newInstance() = HomeFragment()
}
override fun getFragmentName(): String = FRAGMENT_NAME
override fun setTitle() = fragmentChannel?.setTitle(R.string.home)
fun onLoginButtonClick(){
fragmentChannel?.showLogin()
}
override fun dispose(){
TODO("Call presenter dispose method")
}
}
HomeActivity.kt
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
class HomeActivity : AppCompatActivity(), FragmentChannel{
@BindView(R.layout.activity_home_container)
lateinit var flContainer: FrameLayout
val simpleFragmentManager by lazy {
SimpleFragmentManager(getSupportFragmentManager(), flContainer)
}
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
ButterKnife.bind(this)
simpleFragmentManager.add(HomeFragment.newInstance())
}
override fun onSaveInstanceState(outState: Bundle?){
super.onSaveInstanceState(outState)
simpleFragmentManager.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?){
super.onRestoreInstanceState(savedInstanceState)
simpleFragmentManager.onRestoreInstanceState(savedInstanceState)
}
override fun setTitle(titleId: Int) {
supportActionBar?.title = getString(titleId)
}
override fun showLogin(){
simpleFragmentManager.replace(LoginFragment.newInstance())
}
override fun onBackPressed(){
if(!simpleFragmentManager.onBackPressed){
finish();
}
}
}
And last but not least on latest Google IO (2017), guys from Google found it necessary to remind everyone about the basics as well.