Implementing a Custom Bottom Navigation Bar with ViewPager2 on Android

Project Dependencies

To utilize the modern ViewPager2 component, include the following dependency in your build.gradle file:

implementation "androidx.viewpager2:viewpager2:1.0.0"

Base Class Architecture

Establishing a robust architecture requires defining base classes for Activities and Fragments to handle data binding, ViewModels, and common UI states like loading and errors.

Abstract Activity Implementation

public abstract class AppBaseActivity<V extends ViewDataBinding, VM extends BaseViewModel> extends AppCompatActivity {

    protected V binding;
    protected VM viewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        performDependencyInjection();
        
        binding = DataBindingUtil.setContentView(this, getLayoutId());
        viewModel = new ViewModelProvider(this).get(getViewModelClass());
        binding.setLifecycleOwner(this);
        binding.setVariable(BR.viewModel, viewModel);

        setupUiObservers();
        initializeViews();
        
        getLifecycle().addObserver(viewModel);
    }

    protected abstract int getLayoutId();
    protected abstract Class<VM> getViewModelClass();
    protected abstract void initializeViews();
    
    private void setupUiObservers() {
        if (viewModel != null) {
            viewModel.getUiState().observe(this, state -> {
                handleUiState(state);
            });
        }
    }

    protected void handleUiState(UiState state) {
        // Logic to switch between Loading, Error, Content, etc.
    }
    
    protected void performDependencyInjection() {}
}

Abstract Fragment Implementation

public abstract class AppBaseFragment<V extends ViewDataBinding, VM extends BaseViewModel> extends Fragment {

    protected V binding;
    protected VM viewModel;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);
        return binding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(this).get(getViewModelClass());
        binding.setLifecycleOwner(getViewLifecycleOwner());
        binding.setVariable(BR.viewModel, viewModel);
        
        setupViews();
        observeData();
    }

    protected abstract int getLayoutId();
    protected abstract Class<VM> getViewModelClass();
    protected abstract void setupViews();
    protected abstract void observeData();
}

Custom Navigation Implementation

This section demonstrates how to implement a bottom navigation bar using a custom LinearLayout with text indicators linked to a ViewPager2.

Host Activity Logic

public class MainNavController extends AppBaseActivity<ActivityMainBinding, MainNavViewModel> {

    private List<Fragment> tabFragments = new ArrayList<>();
    private BottomNavAdapter pagerAdapter;

    @Override
    protected int getLayoutId() {
        return R.layout.activity_main;
    }

    @Override
    protected Class<MainNavViewModel> getViewModelClass() {
        return MainNavViewModel.class;
    }

    @Override
    protected void initializeViews() {
        setupFragments();
        setupViewPager();
        setupBottomNavigation();
    }

    private void setupFragments() {
        tabFragments.add(new DashboardFragment());
        tabFragments.add(new StoreFragment());
        tabFragments.add(new ServicesFragment());
        tabFragments.add(new ProfileFragment());
    }

    private void setupViewPager() {
        pagerAdapter = new BottomNavAdapter(this, tabFragments);
        binding.mainPager.setAdapter(pagerAdapter);
        binding.mainPager.setOffscreenPageLimit(tabFragments.size());

        binding.mainPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
            @Override
            public void onPageSelected(int position) {
                super.onPageSelected(position);
                updateTabAppearance(position);
            }
        });
    }

    private void setupBottomNavigation() {
        binding.navHome.setOnClickListener(v -> binding.mainPager.setCurrentItem(0, false));
        binding.navStore.setOnClickListener(v -> binding.mainPager.setCurrentItem(1, false));
        binding.navServices.setOnClickListener(v -> binding.mainPager.setCurrentItem(2, false));
        binding.navProfile.setOnClickListener(v -> binding.mainPager.setCurrentItem(3, false));
        
        // Set initial state
        updateTabAppearance(0);
    }

    private void updateTabAppearance(int selectedIndex) {
        int activeColor = Color.parseColor("#FF4081");
        int inactiveColor = Color.parseColor("#808080");

        binding.navHome.setTextColor(selectedIndex == 0 ? activeColor : inactiveColor);
        binding.navStore.setTextColor(selectedIndex == 1 ? activeColor : inactiveColor);
        binding.navServices.setTextColor(selectedIndex == 2 ? activeColor : inactiveColor);
        binding.navProfile.setTextColor(selectedIndex == 3 ? activeColor : inactiveColor);
    }
}

Layout Configuration

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/mainPager"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/bottomNavContainer"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <LinearLayout
            android:id="@+id/bottomNavContainer"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:background="@color/white"
            android:elevation="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent">

            <TextView
                android:id="@+id/navHome"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Home"
                android:gravity="center"
                android:padding="12dp"
                android:textSize="14sp"
                android:clickable="true"
                android:focusable="true" />

            <TextView
                android:id="@+id/navStore"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Store"
                android:gravity="center"
                android:padding="12dp"
                android:textSize="14sp"
                android:clickable="true"
                android:focusable="true" />

            <TextView
                android:id="@+id/navServices"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Services"
                android:gravity="center"
                android:padding="12dp"
                android:textSize="14sp"
                android:clickable="true"
                android:focusable="true" />

            <TextView
                android:id="@+id/navProfile"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Profile"
                android:gravity="center"
                android:padding="12dp"
                android:textSize="14sp"
                android:clickable="true"
                android:focusable="true" />
        </LinearLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Sample Fragment Implementation

public class DashboardFragment extends AppBaseFragment<FragmentDashboardBinding, DashboardViewModel> {

    @Override
    protected int getLayoutId() {
        return R.layout.fragment_dashboard;
    }

    @Override
    protected Class<DashboardViewModel> getViewModelClass() {
        return DashboardViewModel.class;
    }

    @Override
    protected void setupViews() {
        // Initialize specific UI components
    }

    @Override
    protected void observeData() {
        // Observe LiveData from ViewModel
    }
}

Sample Fragment Layout

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#F5F5F5">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Dashboard Screen"
            android:textSize="24sp"
            android:textColor="#333333"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Tags: Android viewpager2 bottom-navigation kotlin java

Posted on Fri, 08 May 2026 23:14:56 +0000 by phpflixnewbie