Skip to content

PandoraMedia/BottomNavigator

Repository files navigation

Build Status

Bottom Navigator

Bottom Navigator is a library for managing multiple Fragment backstacks controlled by the tabs of a BottomNavigationView. It has a simple API and is feature-rich.

Unlike Material Design's Android recommendations, the state of each tab's stack is preserved as the user switches between tabs.

The library keeps a history of previous tabs so that when the current tab's stack is exhausted the system back button will navigate to the previously selected tab. Rotation is automatically handled and all backstacks are preserved.

Table of Contents

How to Use it
Multiple taps on a tab
Detachability
Obtaining a BottomNavigator reference
Dependencies
Gradle

How to Use it

BottomNavigator needs an Activity with a BottomNavigationView and a fragment container.

<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomnav_view"
        android:layout_width="match_parent"
        android:layout_height="56dp"
        app:menu="@menu/navigation_items" />
</LinearLayout>

Then, in the activity's onCreate you initialize the BottomNavigator by calling its onCreate function. You need to provide a rootFragmentsFactory that maps the BottomNavigationView's menu items to the root fragment for each tab. And a defaultTab which is the first tab shown to the user.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        navigator = BottomNavigator.onCreate(
            fragmentContainer = R.id.fragment_container,
            bottomNavigationView = findViewById(R.id.bottomnav_view),
            rootFragmentsFactory = mapOf(
                R.id.tab1 to { RootFragment1() },
                R.id.tab2 to { RootFragment2() },
                R.id.tab3 to { RootFragment3() }
            ),
            defaultTab = R.id.tab2,
            activity = this
        )
    }

Finally, wire up the back button to the BottomNavigator.

    override fun onBackPressed() {
        if (!navigator.pop()) {
            super.onBackPressed()
        }
    }

You can now call methods like navigator.addFragment(fragment) to add a fragment to the current tab's stack, or navigator.switchTab(R.id.tab1) to switch stacks.

Checkout the sample app for a complete example.

Multiple taps on a tab

When a user taps on the currently shown bottom tab the expectation is that the tab's state will be reset. If the tab has a backstack then the stack will be removed leaving the root fragment in whatever state it was in. Another tap when the root Fragment is shown will recreate the fragment and replace it with the new root fragment.

In order to get a smoother user experience you can avoid recreating the root fragment by subscribing to BottomNavigator's resetRootFragmentCommand() rx stream. In the subscription to that stream you should reset the given root fragment's state. For example, you can call smoothScrollToPosition(0) on a RecyclerView in the Fragment, or you might clear the text in a search box.

Detachability

By default as Fragments are hidden and shown as the user navigates around, the fragments are attached and detached which generates the onDestroyView/onCreateView lifecycle on the Fragments. This is good for memory consumption because it allows View objects that are not being seen by the user to be garbage collected and then recreated in onCreateView when the user wants to see them again.

If you do not want hidden Fragments to have their Views destroyed, maybe because of lifecycle issues or for performance, you can mark a Fragment as not being detachable. This will cause the fragments to be hidden/shown without having their Views destroyed. To do this specify detachable = false when adding the fragment, or for root Fragments by initializing BottomNavigator with onCreateWithDetachability and providing FragmentInfo objects to the rootFragmentsFactory.

Obtaining a BottomNavigator reference

BottomNavigator is scoped to an activity session, this means that after rotation a new Activity gets the same instance from BottomNavigator.onCreate as the previous Activity instance. Fragments and other objects with reference to the Activity can obtain the same activity session's navigator instance by calling BottomNavigator.provide(activity). This allows the Fragments to add other Fragments to the BottomNavigator.

Dependencies

BottomNavigator works with AndroidX apps. It is built in Kotlin on top of Architecture Components and RxJava.

Gradle

dependencies {
    implementation 'com.pandora.bottomnavigator:bottom-navigator:1.8'
}

Further reading

https://engineering.pandora.com/announcing-bottom-navigator-64f6e426a6b1

License

Copyright 2019 Pandora Media, LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
See accompanying LICENSE file or you may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.