Android SharedPreferences Tutorial and Example

Using ViewPager2 with TabLayout for Swipeable Tabs

Swipeable tabs are a familiar Android pattern for grouping related screens without forcing users through a long menu. With ViewPager2 and Material Design’s TabLayout, users can tap a tab or swipe between pages while the selected tab stays synchronised with the visible content.

This approach works well for applications that separate information into clear categories, such as news, shopping, transport, or account areas. A local events app could use tabs for Melbourne, Sydney, Brisbane, and Perth, while a travel app might separate ferry routes, train services, and saved journeys.

The example below uses Kotlin and fragments. It explains the required dependencies, creates a pager adapter, connects the tabs, and covers practical details such as state restoration, accessibility, and avoiding unnecessary fragment recreation.

Add the required Android dependencies

ViewPager2 is the modern replacement for the original ViewPager. It is based on RecyclerView, supports horizontal and vertical paging, and behaves more reliably with current Android versions. TabLayout is supplied by the Material Components library.

Add the following dependencies to the app module’s build.gradle file:

dependencies {
    implementation("androidx.viewpager2:viewpager2:1.1.0")
    implementation("com.google.android.material:material:1.12.0")
}

Use versions compatible with the rest of your project if newer releases are available. Your application theme should inherit from a Material theme so that tab colours, typography, ripple effects, and accessibility states are applied consistently.

Create a layout containing a TabLayout above a ViewPager2:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

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

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>

For many tabs, app:tabMode="scrollable" allows the row to move horizontally. This can be useful when a service covers Australian states and territories, although short labels are generally easier to scan on smaller phones.

Create fragments for each tab

Each swipeable page should usually be a fragment. Fragments keep each screen’s layout and behaviour separate, making the project easier to expand than placing every tab in one large activity.

Create simple fragments such as HomeFragment, FavouritesFragment, and SettingsFragment. A reusable fragment can inflate a layout and display the relevant content:

class HomeFragment : Fragment(R.layout.fragment_home)

class FavouritesFragment : Fragment(R.layout.fragment_favourites)

class SettingsFragment : Fragment(R.layout.fragment_settings)

For a real application, each fragment can contain a RecyclerView, a search field, or a database-backed list. A community noticeboard for Fremantle, for example, could keep local posts in one page and saved posts in another without mixing their view logic.

The pager adapter receives the fragment list and creates the page at the requested position:

class MainPagerAdapter(
    fragmentActivity: FragmentActivity,
    private val pages: List<Fragment>
) : FragmentStateAdapter(fragmentActivity) {

    override fun getItemCount(): Int = pages.size

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

FragmentStateAdapter saves and restores fragment state as pages move in and out of memory. This is preferable to manually creating fragments inside onPageSelected, which can cause duplicated screens and lost input.

Connect ViewPager2 and TabLayout

In the activity, create the adapter and assign it to the pager. TabLayoutMediator then links each tab position to its matching page:

class MainActivity : AppCompatActivity() {

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

        val viewPager = findViewById<ViewPager2>(R.id.viewPager)
        val tabLayout = findViewById<TabLayout>(R.id.tabLayout)

        val pages = listOf(
            HomeFragment(),
            FavouritesFragment(),
            SettingsFragment()
        )

        viewPager.adapter = MainPagerAdapter(this, pages)

        TabLayoutMediator(tabLayout, viewPager) { tab, position ->
            tab.text = when (position) {
                0 -> "Home"
                1 -> "Favourites"
                else -> "Settings"
            }
        }.attach()
    }
}

Call attach() after setting the pager adapter. The mediator listens for tab selections and page changes, so tapping “Favourites” moves the pager and swiping from “Home” updates the selected tab automatically.

Avoid calling attach() repeatedly when the activity is recreated or when data changes. If the mediator is stored as a property and replaced, detach the previous mediator first. For fixed pages, the straightforward setup above is usually all that is required.

Preserve data and manage page state

Swipeable navigation should not erase what the user has entered. A fragment’s view may be destroyed while its fragment remains available, so important values should live in a ViewModel rather than only in view references.

For small settings, such as a selected theme or preferred tab, SharedPreferences can be suitable. When the stored value is a Kotlin object rather than a simple string or Boolean, this guide to saving complex objects explains how Gson can serialise and restore it.

Use ViewModel and SavedStateHandle for temporary screen state, including a search query or selected filter. Use a repository and Room database for durable application data. This distinction matters in an app used during a long train trip between Adelaide and Melbourne, where Android may reclaim memory while the user is away from the screen.

You can also control the number of pages retained around the current page:

viewPager.offscreenPageLimit = 1

The default behaviour is generally appropriate. Increasing the limit keeps more fragment views active, which may make small pages feel immediate but can increase memory use. Avoid setting a high value simply to hide slow loading; load data asynchronously instead.

Improve appearance and accessibility

Tab labels should be concise and meaningful. “Saved” may be clearer than “Favourites” for an international audience, while an Australian shopping app might use “Orders” and “Wishlist” according to its established product language. Keep labels consistent with the rest of the interface rather than switching between formal and casual wording.

Icons can be added when they genuinely clarify meaning:

TabLayoutMediator(tabLayout, viewPager) { tab, position ->
    tab.text = listOf("Home", "Saved", "Settings")[position]
    tab.setIcon(
        when (position) {
            0 -> R.drawable.ic_home
            1 -> R.drawable.ic_bookmark
            else -> R.drawable.ic_settings
        }
    )
}.attach()

Do not rely on colour alone to indicate the active tab. Maintain sufficient contrast, provide readable text at larger font sizes, and test with TalkBack enabled. A user checking a bus timetable in Canberra should be able to identify the selected page without depending on subtle indicator colours or fast animations.

If your pages contain forms or horizontally scrolling content, check that swipe gestures do not conflict with nested scrolling. Test on both small and large screens, as well as with Android’s “larger text” setting enabled.

Test common navigation scenarios

Test every tab by tapping it and by swiping slowly and quickly. Confirm that the indicator settles on the correct label after a partial swipe, and check that rotating the device does not return the user to an unexpected page.

You should also test process recreation, backgrounding, and returning through the Android system Back button. If the app displays live information, verify that each fragment handles loading, empty, error, and offline states. This is especially useful for users in regional Queensland or Western Australia, where connectivity can be less dependable than in central Sydney.

For a tab bar with dynamic content, update the underlying page list carefully. Changing the order without stable item identity can make a fragment display the wrong data. If pages are added or removed frequently, consider an adapter based on stable identifiers and update the tab labels at the same time.

A well-configured ViewPager2 with TabLayout gives users a familiar, efficient way to move through related screens. Keeping fragments focused, state in suitable architecture components, and labels accessible will make the interface dependable across phones, tablets, and the varied network conditions common throughout Australia.