Fragment and ViewPager

The Android framework contains a number of UI components that allow a common feel to design simple UIs -- one is the ViewPager. These UI components integrate fragments which are associated with a view. Fragments let us build reusable and extensible UIs.

What this lecture will teach you

Resources

Checkout the demo project

ViewPager app

The demo app is a simple application used to demonstrate the use on the ViewPager and fragments.

Fragments are associated with the UI. We will develop the demo app to best illustrate how fragments can use Android UI components such as ViewPager (more on that in a moment). The demo app will combine multiple fragments (three in all) in a single activity to build a multi-pane UI. These fragments can be considered self-contained components and could be reused by multiple activities potentially across multiple applications. The app is made up of three tabs and three corresponding fragments.

Play with the app to get a feel for the UI -- cool hey? Simple, compact and a nice way to build up the features and services offered by your app. We will use ViewPager and fragment combination as part of Lab2.

There is one activity and four fragments in this project:

Tabs

Tabs in the action bar are often where you put titles for different pages in your app. They make it easy to explore and switch between different views or functional aspects of your app, or to browse categorized data sets. Check out the Google Play app on your phone for a quick demonstration of scrollable tabs, where tab strip indicators move smoothly from tab to tab. Users can gain continuous touch feedback as they swipe between pages.

Developing sliding tabs with TabLayout and ViewPager

Add TabLayout and ViewPager to the main.xml file. We put them into a RelativeLayout where ViewPager is below the TabLayout. At onCreate(), the MainActivity will load these two components to render the sliding tabs and swipeable pages.  

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
...
>

<com.google.android.material.tabs.TabLayout
android:id="@+id/tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_below="@id/tab"
/>
</RelativeLayout>

Creating the ActionBar and Tabs

Our design uses ViewPager2to switch between Fragment objects, and each Fragment is titled by one sliding tab, as shown in the code snippet. ViewPager is most often used in conjunction with Fragment, which is a convenient way to supply and manage the lifecycle of each page. To use ViewPager2, you need to implement a custom FragmentStateAdapter class by overriding two methods (createFragment and getItemCount). The custom FragmentStateAdapter class takes the hosting activity as a parameter and an ArrayList of fragments (e.g., FragmentA, FragmentB, FragmentC) as a property.

class MyFragmentStateAdapter(activity: FragmentActivity, var list: ArrayList<Fragment>) :
FragmentStateAdapter(activity){

override fun createFragment(position: Int): Fragment {
return list[position]
}

override fun getItemCount(): Int {
return list.size
}
}

In Android, adapters are a bridge between Adapter View (e.g., ViewPager, ListView, etc.) and the underlying data sources for that view (e.g., arraylists, databases, fragments, etc.). Once the FragmentStateAdapter is set up properly, we create three new fragments. These are all pretty much identical code. Each fragment inflates its own layout (e.g., R.layout.fragment_a) in onCreateView(). We then add the fragment to an ArrayList.

class MainActivity : AppCompatActivity() {

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

viewPager2 = findViewById(R.id.viewpager)
tabLayout = findViewById(R.id.tab)

fragmentA = FragmentA()
fragmentB = FragmentB()
fragmentC = FragmentC()

fragments = ArrayList()
fragments.add(fragmentA)
fragments.add(fragmentB)
fragments.add(fragmentC)

myMyFragmentStateAdapter = MyFragmentStateAdapter(this, fragments)
viewPager2.adapter = myMyFragmentStateAdapter

val tabConfigurationStrategy = TabConfigurationStrategy { tab, position -> tab.text = tabTitles[position] }

val tabLayoutMediator = TabLayoutMediator(tabLayout, viewPager2, tabConfigurationStrategy)
tabLayoutMediator.attach()

}
}
Next, we create a MyFragmentStateAdapter object, assign it to the builtin adapter of ViewPager2.
myMyFragmentStateAdapter = MyFragmentStateAdapter(this, fragments)
viewPager2.adapter = myMyFragmentStateAdapter

Next, to link the TabLayout and ViewPager2, you have to use TabLayoutMediator. This synchronizes the ViewPager and TabLayout to change the position when one gets clicked or swiped.See this documentation to learn more about it. The TabLayoutMediator expects three parameters: TabLayout, ViewPager2 and an interface called TabConfigurationStrategy. Don't forget to attach(), which links ViewPager2 and TabLayout together.

val tabLayoutMediator = TabLayoutMediator(tabLayout, viewPager2, tabConfigurationStrategy)
tabLayoutMediator.attach()

Lastly, let's look at TabConfigurationStrategy. It sets the title for each of the tabs. Here the tabTitles array has the titles for each fragment arranged by their order.

val tabConfigurationStrategy = TabConfigurationStrategy { tab, position -> tab.text = tabTitles[position] }

Fragments

Up until now we have used activities as drivers to display views to the screen. The previous example each screen is associated with an activity that is used to render and manage the user interaction with the view. All that changes in this lecture. We move away from activities and start to use fragments -- a fragment is now used to render and manager a particular UI view. Think of fragments as lightweight activities for now. Your application still needs activities but we don't use them to solely render views. Most applications, and ones we will develop, include one or more activity and fragments.

What are fragments?

In implementation there are considerable differences; for example, an activity has to be specified in the manifest, while fragments do not because they can only exist in the context of an activity and are coupled to the activity state. While fragments are strongly integrated into the UI you can use fragments without a view or UI components. Fragments can implement a number of application behaviors, such as, code to handle listeners, files, database, etc. These types of fragments can be thought of as invisible worker for the activity -- more later on this. For now we will just consider fragments when applied to the UI.

Why use fragments over activities?

A fragment is a modular part of an activity. It has its own lifecycle and receives its own input events. Fragments can created, added or removed while the activity is running. Consider a fragment a "sub activity" that you can reuse in different activities. But you could ignore fragments and just use activities if you wish -- but not recommend. Why use fragments over activities? Well fragments are wired to create reusable Android user inferences components -- however, it is true to say you can implement similar application UIs with a pure activity solution, which might be confusing for students. Here are some reasons why you should use fragments as a designer:

Consider the diagram below. You are designing a cool new app that you want gazillions of people to use on all sorts of phones and tablets with different sized screens. The figure shows how your UI based on fragment design could run on a tablet and phone UI; in the case of the tablet with larger screen real estate the UI fragment components are rendered side by side but on the phone with a smaller form factor the two fragments are treated differently and brought into focus at different times. The activity that handles the UI is smart enough to adapt the UI to the type of device.

Fragment and activities akin to processes and threads

Fragments are reusable UI but can also be considered lightweight in how they handle operations on the UI. As I mentioned before you can do most things with activities that fragments do but consider for the moment (if you did this) the relationship between design using all processes or threaded design. Fragments can be considered to be threads in such a choice, as shown below.

Fragment Lifecycle

Though Fragment defines its own lifecycle, that lifecycle is dependent on its activity: if the activity is stopped, no fragments inside of it can be started; when the activity is destroyed, all fragments will be destroyed.

A fragment runs with in the activity and the activity lifecycle that we discussed. So if an activity is stopped so are all its fragments. Similarly if the activity is destroyed so are all the associated fragments. However, when an activity is running (i.e., onResume() state) fragments can be independently managed -- added, replaced or removed. 

The fragment lifecycle diagram shown below looks complicated but as we discuss it consider that this set of fragment states run inside of the running state of the activity lifecycle diagram we discussed in earlier lecture on activity lifecycle. Therefore the state diagram shown below is only valid when the activity associated with the fragment

Like the activity lifecycle the fragment lifecycle is represented by a bunch of states and associated event handlers, which are triggered when a fragment is:

In addition there are a number of callbacks associated with a number of fragment specific events, such as, 1) binding and unbinding a fragment from its activity; 2) when a fragment is created and destroyed. If you look at the core states shown in the figure you see:

Fragment lifecycle events

Many events can occur between these states -- for example the user can hit the back button.

If we consider the demo app it has three fragments - one for each tab view. As the use taps through the tab options the fragment transitions through the states in the lifecycle diagram -- moving say from being in focus in onResume() to hidden in onPause(). But the complete fragment lifecycle is tightly coupled to the activity but also governed by the user interaction with the UI (as is the case with the demo app). The activity can dynamically construct the UI and its associated fragments -- such as adding and removing fragments where each fragment transitions through its full, visible and active lifecycle several times within the lifetime of its parent activity -- for example as a fragment is added and removed from the view.

onAttach() and onDetach() events

The lifecycle starts and ends when a fragment is attached and detached from its parent activity. The onAttach() event occurs before the fragment's UI is created and before the fragment or its parent activity have completed initialization. Keep this in mind when we are looking at an activity's onCreate() when it creates its fragments. It is a good idea to create fragments in the activities onCreate() to ensure they are created only once in the fragment lifetime.

onCreateView() event

The fragment's UI is created and destroyed in onCreateView() and onDestroyView() callbacks, respectively. The onCreateView() method initializes the fragment by inflating the UI, getting references and binding any data to the views that it contains -- and then any addition initialization such as setting up services (we will talk about services later on in the course).

The onCreateView()'s main job in code we will write is a one liner -- taken from FragmentA.kt in the demo app:

return inflater.inflate(R.layout.fragment_a, container, false)

An example of a fragment layout

The R.layout.fragment_a is the ID for an XML layout resource to load (e.g., R.layout.fragment_a). This is defined in the res/layout/fragment_a.xml of the demo project. This specifies the UI for the fragment, which is just a simple TextView but could be a sophisticated view group.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="28sp"
android:text="This is Fragment A" />
</LinearLayout>

Creating fragments and inflating: FragmentA.kt example

A fragment extends the Fragment class as shown below. When the MainActivity creates the fragments it calls onCreateView() for all fragments. All onCreateView() creates and returns the view hierarchy associated with the fragment-- essentially it returns the view for the fragment's UI. The code below overrides the onCreateView() handler into inflate and return the requires view hierarchy; that is, R.layout.fragment_a, as shown above. Note, if your fragment does not use a UI (such as providing the activity with background behavior) you do not inflate of course.

Importantly, fragments do not have to be registered in the manifest like activities are. And, as the fragment lifecycle, fragments are tied to the state of the activity so this makes good sense; that is, they can't exist without an activity.

class FragmentA : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_a, container, false)
}
}