Posts Simplified Fragment Management
Post
Cancel

Simplified Fragment Management

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.

  1. 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 get fragment.size() == 3 but actual number of fragments is two and third position fragments.get(fragments.size() - 1) will return null.

    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 call fragments.get(0) and wow, it returns null, 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 contain null values on any position and we’ll end up with wrong fragment or with java.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.

  2. Handling onBackPressed navigation and application title update

    Typical android application shows Toolbar on top of the screen. If the Toolbar is part of the Activity layout we need to update its title 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 or java.lang.IllegalStateException: Fragment LoginFragment not attached to Activity and honestly, I’ve stopped looking for reasons why. If you move getActivity().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 the Fragment which is popping up and at some point setTitle() method might not get called at all, and you end up with title in the Toolbar 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.

  3. 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.

  4. 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 which Activity implements, I know, obvious.

Simple solution I came up with have four main components:

  • SimpleFragmentManager which is basically a wrapper around FragmentManager and exposes the methods for add, replace, popUp, popUpAll and getCurrentFragment operations. SimpleFragmentManager assumes that each Fragment implements IFragment interface.

  • IFragment interface is forcing each Fragment to have its own unique name or tag under which is added to transaction, dispose() method (which is very handy when implementing RxJava2), and yes, method to setTitle() of the Toolbar/ActionBar.

  • FragmentTagStack to handle stack easily and retrieve fragments. It does not contain fragment instances, but its tags as String values instead and have only basic methods for pop, popUpAll, peek, push and getActiveTag which returns tag of currently shown Fragment on the screen.

  • FragmentChannel is the most basic interface for callback implementation between the Fragment and hosting environment, which can be Activity or parent Fragment.

And here is the sequence diagram that simplifies the explanation, that shows basic interaction with adding, replacing and removing fragments.

Sequence Diagram

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.

  1. Activity initialize SimpleFragmentManager.
    • Override onSaveInstanceState and onRestoreInstanceState methods and forward the state to SimpleFragmentManager.
    • Override onBackPressed method forward the call to SimpleFragmentManager.
    • Implement FragmentChannel.
  2. Each Fragment implements IFragment interface. Tip: Usually, having abstract BaseFragment class which implements IFragment and initializes FragmentChannel 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.

This post is licensed under CC BY 4.0 by the author.