Module 08: Architecture Patterns & Navigation
Objective: Understand common architecture patterns in Android projects (MVC, MVP, MVVM), and master ViewModel, LiveData, and the Jetpack Navigation component.
Frontend Comparison: Architecture Evolution
| Frontend Architecture | Android Architecture | Core Idea |
|---|---|---|
| jQuery spaghetti code | MVC (raw) | No separation; everything crammed into the Activity |
| Redux / Vuex | MVP | Clear separation of View and Presenter |
| React + Hooks / Zustand | MVVM | Data-driven UI, reactive |
1. MVC Pattern (Earliest Era)
The most common "pseudo-MVC" pattern found in legacy projects (circa 2015). In practice, all logic is crammed into the Activity:
// ❌ Classic "God Activity": acts as both Controller and View
public class UserListActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_list);
// Model: data structure defined right here
// View: UI manipulation right here
// Controller: business logic right here
// All mixed together!
new Thread(() -> {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://api.example.com/users")
.build();
try {
Response response = client.newCall(request).execute();
String json = response.body().string();
List<User> users = new Gson().fromJson(json,
new TypeToken<List<User>>(){}.getType());
runOnUiThread(() -> {
RecyclerView rv = findViewById(R.id.rv_users);
rv.setLayoutManager(new LinearLayoutManager(this));
rv.setAdapter(new UserAdapter(users));
});
} catch (IOException e) {
runOnUiThread(() -> {
Toast.makeText(this, "Failed to load", Toast.LENGTH_SHORT).show();
});
}
}).start();
}
}2. MVP Pattern (Mainstream 2015-2019)
MVP is the most widely adopted architecture pattern in Android and the most common in legacy projects:
View (Activity/Fragment) ←→ Presenter ←→ Model (Repository/API)
↑ ↑
UI concerns only Business logic and data handling2.1 Defining the Contract
// Define View and Presenter interfaces (the contract)
public interface UserListContract {
interface View {
void showLoading();
void hideLoading();
void showUsers(List<User> users);
void showError(String message);
void navigateToDetail(User user);
}
interface Presenter {
void attachView(View view);
void detachView();
void loadUsers();
void onUserClicked(User user);
}
}2.2 Presenter Implementation
public class UserListPresenter implements UserListContract.Presenter {
private UserListContract.View view;
private UserRepository repository;
public UserListPresenter(UserRepository repository) {
this.repository = repository;
}
@Override
public void attachView(UserListContract.View view) {
this.view = view;
}
@Override
public void detachView() {
this.view = null; // Prevent memory leaks
}
@Override
public void loadUsers() {
if (view == null) return;
view.showLoading();
repository.getUsers(new DataCallback<List<User>>() {
@Override
public void onSuccess(List<User> users) {
if (view != null) {
view.hideLoading();
view.showUsers(users);
}
}
@Override
public void onError(Throwable error) {
if (view != null) {
view.hideLoading();
view.showError(error.getMessage());
}
}
});
}
@Override
public void onUserClicked(User user) {
if (view != null) {
view.navigateToDetail(user);
}
}
}2.3 View Implementation (Activity)
public class UserListActivity extends AppCompatActivity
implements UserListContract.View {
private UserListPresenter presenter;
private ProgressBar progressBar;
private RecyclerView recyclerView;
private UserAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_list);
// Initialize UI
progressBar = findViewById(R.id.progress_bar);
recyclerView = findViewById(R.id.rv_users);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new UserAdapter(new ArrayList<>(), this::onUserClicked);
recyclerView.setAdapter(adapter);
// Create the Presenter
presenter = new UserListPresenter(new UserRepository());
presenter.attachView(this);
presenter.loadUsers();
}
private void onUserClicked(User user) {
presenter.onUserClicked(user);
}
// ---- View interface implementation ----
@Override
public void showLoading() {
progressBar.setVisibility(View.VISIBLE);
}
@Override
public void hideLoading() {
progressBar.setVisibility(View.GONE);
}
@Override
public void showUsers(List<User> users) {
adapter.updateUsers(users);
}
@Override
public void showError(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
@Override
public void navigateToDetail(User user) {
Intent intent = new Intent(this, UserDetailActivity.class);
intent.putExtra("user_id", user.getId());
startActivity(intent);
}
@Override
protected void onDestroy() {
super.onDestroy();
presenter.detachView(); // Prevent leaks
}
}3. MVVM Pattern (Mainstream 2019+, Recommended)
MVVM uses data to drive the UI. The ViewModel holds the data, and the View observes data changes:
View (Activity/Fragment) ←observes→ ViewModel ←→ Repository
↑ ↑
UI and events only Holds UI data, survives config changes3.1 ViewModel
public class UserListViewModel extends ViewModel {
private final UserRepository repository;
// MutableLiveData: mutable inside the ViewModel
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>();
private final MutableLiveData<List<User>> users = new MutableLiveData<>();
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
// Expose as LiveData (read-only) to the outside
public LiveData<Boolean> getIsLoading() { return isLoading; }
public LiveData<List<User>> getUsers() { return users; }
public LiveData<String> getErrorMessage() { return errorMessage; }
public UserListViewModel() {
this.repository = new UserRepository();
}
public void loadUsers() {
isLoading.setValue(true);
repository.getUsers(new DataCallback<List<User>>() {
@Override
public void onSuccess(List<User> result) {
isLoading.setValue(false);
users.setValue(result);
}
@Override
public void onError(Throwable error) {
isLoading.setValue(false);
errorMessage.setValue(error.getMessage());
}
});
}
}3.2 Activity Observing the ViewModel
public class UserListActivity extends AppCompatActivity {
private UserListViewModel viewModel;
private UserAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_list);
// Obtain ViewModel (not recreated on configuration changes)
viewModel = new ViewModelProvider(this).get(UserListViewModel.class);
// Initialize UI
RecyclerView rv = findViewById(R.id.rv_users);
ProgressBar progressBar = findViewById(R.id.progress_bar);
rv.setLayoutManager(new LinearLayoutManager(this));
adapter = new UserAdapter(new ArrayList<>());
rv.setAdapter(adapter);
// Observe data changes
viewModel.getIsLoading().observe(this, loading -> {
progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
});
viewModel.getUsers().observe(this, userList -> {
adapter.updateUsers(userList);
});
viewModel.getErrorMessage().observe(this, message -> {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
});
// Initial load
if (savedInstanceState == null) {
viewModel.loadUsers();
}
}
}3.3 LiveData Key Points
// Core characteristics of LiveData:
// 1. Lifecycle-aware: stops sending updates after the Activity is destroyed, preventing crashes
// 2. Sticky events: new observers receive the last emitted value upon registration
// 3. Automatic cleanup: no need to manually unregister observers
// setValue(): must be called on the main thread
liveData.setValue(newValue);
// postValue(): can be called from a background thread
liveData.postValue(newValue);
// Data transformation (similar to JS .map())
LiveData<String> userName = Transformations.map(userLiveData, user -> user.getName());
// Combining multiple LiveData sources (similar to Promise.all)
MediatorLiveData<Result> mediator = new MediatorLiveData<>();
mediator.addSource(usersLiveData, users -> combine(users, otherData));
mediator.addSource(otherLiveData, other -> combine(users, other));Common LiveData Pitfalls
// ❌ Wrong: ViewModel holding an Activity/Fragment reference
public class UserViewModel extends ViewModel {
private Activity activity; // memory leak!
}
// ✅ Correct: ViewModel doesn't reference View; exposes data via LiveData
// ❌ Wrong: wrapping click events in LiveData (triggers again after rotation)
public class LoginViewModel extends ViewModel {
private MutableLiveData<Boolean> loginClick = new MutableLiveData<>();
}
// ✅ Correct: one-time events use SingleLiveEvent or manual callbacks
public class SingleLiveEvent<T> extends MutableLiveData<T> {
// Use AtomicBoolean to ensure one-time consumption
}
// ✅ Correct: update from background thread with postValue
new Thread(() -> {
List<User> users = repository.loadUsers();
usersLiveData.postValue(users); // safe from background thread
}).start();3.4 Repository Pattern
Repository is the intermediary in MVVM that connects the ViewModel to data sources (network, database, cache):
public class UserRepository {
private final UserApi api;
private final UserDao dao;
public UserRepository(UserApi api, UserDao dao) {
this.api = api;
this.dao = dao;
}
public LiveData<List<User>> getUsers() {
// 1. Return local cache first
// 2. Fetch from network in background
// 3. Update database on success
// 4. Room notifies UI automatically via LiveData
refreshUsers();
return dao.getAllUsers();
}
private void refreshUsers() {
new Thread(() -> {
try {
List<User> users = api.fetchUsers().execute().body();
dao.insertAll(users);
} catch (IOException e) {
Log.e("UserRepository", "Failed to refresh users", e);
}
}).start();
}
}Frontend analogy: Repository is like combining
services/api.ts+stores/cachein a frontend project, responsible for unified data fetching and caching.
4. Architecture Identification Guide
When taking over a legacy project, here's how to tell which architecture is being used:
| Indicator | Architecture | File Naming |
|---|---|---|
| Network requests and UI logic written directly in Activity | MVC (no architecture) | Only Activity files |
XxxContract and XxxPresenter interfaces/classes present | MVP | UserContract.java, UserPresenter.java |
XxxViewModel + LiveData present | MVVM | UserViewModel.java |
| Dagger/Hilt dependency injection present | Usually MVP or MVVM | XxxModule.java, @Inject |
@HiltAndroidApp, @Inject present | MVVM + DI | XxxModule.java |
5. Jetpack Navigation Component
5.1 Navigation Graph
<!-- res/navigation/nav_graph.xml -->
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.example.app.HomeFragment"
android:label="Home">
<action
android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left" />
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.example.app.DetailFragment"
android:label="Detail">
<argument
android:name="userId"
app:argType="integer" />
</fragment>
<fragment
android:id="@+id/settingsFragment"
android:name="com.example.app.SettingsFragment"
android:label="Settings" />
</navigation>5.2 Hosting Navigation in an Activity
<!-- res/layout/activity_main.xml -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />5.3 Navigation Operations
// Navigate to another destination from a Fragment
Navigation.findNavController(view)
.navigate(R.id.action_home_to_detail);
// Pass arguments
Bundle args = new Bundle();
args.putInt("userId", 123);
Navigation.findNavController(view)
.navigate(R.id.action_home_to_detail, args);
// Receive arguments in the target Fragment
int userId = getArguments().getInt("userId");
// Navigate back
Navigation.findNavController(view).navigateUp();
// Wire up with bottom navigation bar
BottomNavigationView bottomNav = findViewById(R.id.bottom_nav);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupWithNavController(bottomNav, navController);5.4 Safe Args: Type-Safe Navigation
// Project-level build.gradle
plugins {
id 'androidx.navigation.safeargs' version '2.7.6' apply false
}
// Module-level build.gradle
plugins {
id 'androidx.navigation.safeargs'
}// The generated class provides type-safe navigation
HomeFragmentDirections.ActionHomeToDetail action =
HomeFragmentDirections.actionHomeToDetail()
.setUserId(123);
Navigation.findNavController(view).navigate(action);6. Architecture Libraries Commonly Found in Legacy Projects
| Library | Description | Identifying Markers |
|---|---|---|
| Moxy | MVP framework | @InjectPresenter, @BindViewState |
| Mosby | MVP framework | MvpActivity, MvpPresenter |
| Conductor | Controller-based | Controller as a Fragment replacement |
| Dagger 2 | Dependency injection | @Module, @Component, @Inject |
| Hilt | Dependency injection (simplified Dagger) | @HiltAndroidApp, @AndroidEntryPoint |
7. Advanced ViewModel Usage
7.1 ViewModel + SavedStateHandle
public class UserDetailViewModel extends ViewModel {
private final SavedStateHandle handle;
public UserDetailViewModel(SavedStateHandle handle) {
this.handle = handle;
// Retrieve from navigation arguments
int userId = handle.get("userId");
loadUser(userId);
}
// Save state (restored after process death)
public void saveDraft(String text) {
handle.set("draft", text);
}
public String getDraft() {
return handle.get("draft");
}
}7.2 ViewModel Factory
// When the ViewModel constructor requires arguments
public class ViewModelFactory implements ViewModelProvider.Factory {
private final UserRepository repository;
public ViewModelFactory(UserRepository repository) {
this.repository = repository;
}
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
if (modelClass.isAssignableFrom(UserListViewModel.class)) {
return (T) new UserListViewModel(repository);
}
throw new IllegalArgumentException("Unknown ViewModel class");
}
}
// Usage
UserListViewModel viewModel = new ViewModelProvider(this,
new ViewModelFactory(repository)).get(UserListViewModel.class);7.3 LifecycleObserver: Lifecycle-Aware Components
// Let ordinary classes respond to Activity/Fragment lifecycle events
public class MyLocationListener implements DefaultLifecycleObserver {
private final LocationManager manager;
public MyLocationListener(LocationManager manager) {
this.manager = manager;
}
@Override
public void onResume(LifecycleOwner owner) {
manager.start();
}
@Override
public void onPause(LifecycleOwner owner) {
manager.stop();
}
}
// Register in Activity/Fragment
getLifecycle().addObserver(new MyLocationListener(locationManager));Frontend analogy: Similar to React's
useEffect(() => { start(); return () => stop(); }, []), automatically executing and cleaning up when the component mounts/unmounts.
Frontend Developer Cheat Sheet
| Frontend Concept | Android Equivalent | Notes |
|---|---|---|
| Component-local state | Activity member variables | Simplest form of data storage |
| Redux Store | ViewModel + LiveData | Data-driven UI |
useSelector() | liveData.observe() | Observe data changes |
dispatch(action) | viewModel.method() | Trigger state updates |
| React Router | Jetpack Navigation | Screen navigation |
useNavigate() | NavController.navigate() | Programmatic navigation |
useParams() | getArguments() / Safe Args | Route parameters |
<Outlet /> | FragmentContainerView | Nested route container |
| Context Provider | ViewModelProvider | Shared data |
zustand / jotai | ViewModel + LiveData | Lightweight state management |
Next Steps
Now that you understand architecture patterns, move on to Module 09: Debugging, Testing & Troubleshooting to pick up practical debugging and troubleshooting skills.