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>