Building Dynamic UIs: A Guide to Fragments and ViewPager in Android (Java)

In modern Android development, creating flexible and responsive user interfaces that work across various screen sizes is paramount. Two fundamental components that enable this are Fragments, which represent reusable portions of your UI, and ViewPager, which allows users to swipe horizontally between different pages of content. When combined, they form a powerful pattern for building apps like tabbed interfaces, onboarding flows, and detail-detail views.

This article provides a comprehensive guide to implementing a tabbed interface using Fragments with ViewPager in Java.


Core Concepts

1. Fragments

A Fragment is a modular section of an activity with its own lifecycle and input events. You can combine multiple fragments in a single activity to build a multi-pane UI and reuse a fragment in multiple activities.

Key Fragment Lifecycle Methods:

  • onCreateView(): Called to have the fragment instantiate its user interface view.
  • onViewCreated(): Called immediately after onCreateView() where you can setup views (e.g., findViewById).
  • onDestroyView(): Called when the fragment's view is being destroyed.

2. ViewPager and ViewPager2

  • ViewPager (Legacy): The original widget that allows swiping between pages. It requires a PagerAdapter.
  • ViewPager2 (Recommended): A modern replacement for ViewPager, built on RecyclerView. It offers several improvements:
    • Vertical orientation support.
    • Right-to-left (RTL) layout support.
    • Built-in DiffUtil support for better item change animations.
    • It requires a RecyclerView.Adapter or more specifically, a FragmentStateAdapter.

We will use ViewPager2 as it is the current best practice.


Implementation: Building a Tabbed Interface

Let's build a simple app with a ViewPager2 that swipes between three fragments, each displaying a different color and title.

Step 1: Add Dependencies

Ensure you have the necessary dependencies in your app-level build.gradle file.

dependencies {
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.6.1' // For TabLayout
implementation 'androidx.fragment:fragment:1.5.5'
}

Step 2: Create the Fragment Layouts and Classes

Create a simple layout for your fragment, fragment_example.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:background="@color/your_background_color">
<TextView
android:id="@+id/fragment_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Fragment Title"
android:textSize="24sp" />
</LinearLayout>

Create the corresponding Fragment class, ExampleFragment.java:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class ExampleFragment extends Fragment {
private static final String ARG_TITLE = "param1";
private static final String ARG_BG_COLOR = "param2";
private String mTitle;
private int mBgColor;
// Factory method to create a new instance of this fragment
public static ExampleFragment newInstance(String title, int bgColorResId) {
ExampleFragment fragment = new ExampleFragment();
Bundle args = new Bundle();
args.putString(ARG_TITLE, title);
args.putInt(ARG_BG_COLOR, bgColorResId);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mTitle = getArguments().getString(ARG_TITLE);
mBgColor = getArguments().getInt(ARG_BG_COLOR);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_example, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
TextView titleTextView = view.findViewById(R.id.fragment_title);
titleTextView.setText(mTitle);
view.setBackgroundColor(getResources().getColor(mBgColor, requireActivity().getTheme()));
}
}

Step 3: Create the FragmentStateAdapter

The adapter is the bridge between the ViewPager2 and the data (Fragments) it displays. It's responsible for creating the Fragment for each page.

Create ViewPagerAdapter.java:

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import java.util.ArrayList;
import java.util.List;
public class ViewPagerAdapter extends FragmentStateAdapter {
private final List<FragmentInfo> mFragmentInfoList = new ArrayList<>();
// A simple data class to hold fragment information
public static class FragmentInfo {
public final String title;
public final int bgColorResId;
public FragmentInfo(String title, int bgColorResId) {
this.title = title;
this.bgColorResId = bgColorResId;
}
}
public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
public void addFragment(FragmentInfo fragmentInfo) {
mFragmentInfoList.add(fragmentInfo);
}
@NonNull
@Override
public Fragment createFragment(int position) {
FragmentInfo info = mFragmentInfoList.get(position);
// Use the factory method to create a new fragment instance
return ExampleFragment.newInstance(info.title, info.bgColorResId);
}
@Override
public int getItemCount() {
return mFragmentInfoList.size();
}
}

Step 4: Setup the Main Activity Layout and Class

Create the main activity layout, activity_main.xml:

<?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/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="fixed" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

Now, implement the MainActivity.java:

import android.graphics.Color;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
public class MainActivity extends AppCompatActivity {
private ViewPager2 mViewPager;
private ViewPagerAdapter mAdapter;
private TabLayout mTabLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Initialize Views
mViewPager = findViewById(R.id.view_pager);
mTabLayout = findViewById(R.id.tab_layout);
// Setup Adapter
mAdapter = new ViewPagerAdapter(this);
mAdapter.addFragment(new ViewPagerAdapter.FragmentInfo("Red", android.R.color.holo_red_light));
mAdapter.addFragment(new ViewPagerAdapter.FragmentInfo("Green", android.R.color.holo_green_light));
mAdapter.addFragment(new ViewPagerAdapter.FragmentInfo("Blue", android.R.color.holo_blue_light));
mViewPager.setAdapter(mAdapter);
// Connect TabLayout with ViewPager2
new TabLayoutMediator(mTabLayout, mViewPager,
(tab, position) -> tab.setText(mAdapter.mFragmentInfoList.get(position).title)
).attach();
}
}

Key Implementation Details Explained

  1. FragmentStateAdapter vs. FragmentStatePagerAdapter:
    • FragmentStateAdapter (for ViewPager2) is analogous to the old FragmentStatePagerAdapter. It only keeps the current and adjacent fragments in memory, destroying others and saving their state. This is memory-efficient for a large number of pages.
    • The counterpart for ViewPager was FragmentPagerAdapter, which keeps every fragment it ever instantiated in memory, which can be inefficient.
  2. TabLayoutMediator: This utility class seamlessly synchronizes the TabLayout with the ViewPager2. The (tab, position) -> ... lambda is where you configure each tab based on the data at that position.
  3. Fragment Factory Pattern: Using a newInstance factory method in the Fragment is a best practice for passing arguments. It ensures all required parameters are bundled correctly, avoiding issues with the empty constructor required by the system.
  4. Lifecycle Awareness: ViewPager2 and FragmentStateAdapter are fully lifecycle-aware. Fragments will receive correct lifecycle callbacks as they are created, become visible, and are destroyed.

Advanced Considerations

  • Dynamic Data: To update the fragments in the adapter, modify the underlying list (mFragmentInfoList) and call notifyDataSetChanged() on the adapter. For more efficient updates, consider using DiffUtil.
  • Saving State: FragmentStateAdapter automatically saves the state of its fragments. For complex state, you should implement onSaveInstanceState() in your fragments.
  • Offscreen Page Limit: You can control how many adjacent pages are kept in memory using mViewPager.setOffscreenPageLimit(1). The default is 1.

Conclusion

The combination of Fragments and ViewPager2 provides a robust, scalable, and user-friendly pattern for creating swipeable interfaces in Android. By following this guide, you can effectively separate your UI into modular components (Fragments) and present them in a fluid, paged layout. Remember to use ViewPager2 over the legacy ViewPager and leverage the TabLayoutMediator for a polished tabbed experience, ensuring your app is built with modern Android development practices.


Further Reading: Explore the OnPageChangeCallback of ViewPager2 for more granular control over page transitions and the DiffUtil utility for efficiently updating your FragmentStateAdapter.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper