Skip to content

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 ArchitectureAndroid ArchitectureCore Idea
jQuery spaghetti codeMVC (raw)No separation; everything crammed into the Activity
Redux / VuexMVPClear separation of View and Presenter
React + Hooks / ZustandMVVMData-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:

java
// ❌ 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 handling

2.1 Defining the Contract

java
// 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

java
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)

java
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
    }
}

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 changes

3.1 ViewModel

java
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

java
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

java
// 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

java
// ❌ 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):

java
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/cache in 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:

IndicatorArchitectureFile Naming
Network requests and UI logic written directly in ActivityMVC (no architecture)Only Activity files
XxxContract and XxxPresenter interfaces/classes presentMVPUserContract.java, UserPresenter.java
XxxViewModel + LiveData presentMVVMUserViewModel.java
Dagger/Hilt dependency injection presentUsually MVP or MVVMXxxModule.java, @Inject
@HiltAndroidApp, @Inject presentMVVM + DIXxxModule.java

5. Jetpack Navigation Component

5.1 Navigation Graph

xml
<!-- 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

xml
<!-- 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

java
// 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

groovy
// Project-level build.gradle
plugins {
    id 'androidx.navigation.safeargs' version '2.7.6' apply false
}

// Module-level build.gradle
plugins {
    id 'androidx.navigation.safeargs'
}
java
// 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

LibraryDescriptionIdentifying Markers
MoxyMVP framework@InjectPresenter, @BindViewState
MosbyMVP frameworkMvpActivity, MvpPresenter
ConductorController-basedController as a Fragment replacement
Dagger 2Dependency injection@Module, @Component, @Inject
HiltDependency injection (simplified Dagger)@HiltAndroidApp, @AndroidEntryPoint

7. Advanced ViewModel Usage

7.1 ViewModel + SavedStateHandle

java
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

java
// 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

java
// 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 ConceptAndroid EquivalentNotes
Component-local stateActivity member variablesSimplest form of data storage
Redux StoreViewModel + LiveDataData-driven UI
useSelector()liveData.observe()Observe data changes
dispatch(action)viewModel.method()Trigger state updates
React RouterJetpack NavigationScreen navigation
useNavigate()NavController.navigate()Programmatic navigation
useParams()getArguments() / Safe ArgsRoute parameters
<Outlet />FragmentContainerViewNested route container
Context ProviderViewModelProviderShared data
zustand / jotaiViewModel + LiveDataLightweight 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.

A Java Android development tutorial for frontend developers