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 afteronCreateView()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 aPagerAdapter.ViewPager2(Recommended): A modern replacement for ViewPager, built onRecyclerView. It offers several improvements:- Vertical orientation support.
- Right-to-left (RTL) layout support.
- Built-in
DiffUtilsupport for better item change animations. - It requires a
RecyclerView.Adapteror more specifically, aFragmentStateAdapter.
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
- FragmentStateAdapter vs. FragmentStatePagerAdapter:
FragmentStateAdapter(for ViewPager2) is analogous to the oldFragmentStatePagerAdapter. 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.
- TabLayoutMediator: This utility class seamlessly synchronizes the
TabLayoutwith theViewPager2. The(tab, position) -> ...lambda is where you configure each tab based on the data at that position. - Fragment Factory Pattern: Using a
newInstancefactory 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. - Lifecycle Awareness:
ViewPager2andFragmentStateAdapterare 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 callnotifyDataSetChanged()on the adapter. For more efficient updates, consider usingDiffUtil. - Saving State:
FragmentStateAdapterautomatically saves the state of its fragments. For complex state, you should implementonSaveInstanceState()in your fragments. - Offscreen Page Limit: You can control how many adjacent pages are kept in memory using
mViewPager.setOffscreenPageLimit(1). The default is1.
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.