Skip to content

模块 08:架构模式与导航

目标:理解 Android 项目中常见的架构模式(MVC、MVP、MVVM),掌握 ViewModel、LiveData 和 Jetpack Navigation 组件。

学习目标

读完这一章,你将能够:

  • [ ] 说出"God Activity"问题:为什么不能把网络、UI、业务逻辑全堆在一个 Activity 里
  • [ ] 区分 MVC、MVP、MVVM 三种架构各自解决了什么问题
  • [ ] 写出一个 MVP 的契约接口(Contract)和 Presenter,并解释为什么要 detachView 防泄漏
  • [ ] 用 ViewModel + LiveData 实现"数据驱动 UI",并理解 ViewModel 为什么能在屏幕旋转后存活
  • [ ] 看懂接手老项目时如何通过文件命名判断它用了哪种架构
  • [ ] 用 Jetpack Navigation 的导航图 + NavController 实现页面跳转和参数传递

学习建议:架构这一章概念偏抽象,建议边读边对照每段后面的代码示例——三种架构都会用同一份"加载用户列表"的例子重写一遍,对比着看写法差异,比单读文字有效得多。

概念入门:为什么 App 代码要分架构?

刚学 Android 的人最自然的写法,是把所有代码都塞进一个 Activity 里:联网、解析 JSON、刷新列表、处理点击、跳转页面……一个文件几百上千行。这种写法在功能简单时还能跑,但只要项目稍微长大一点就会变成一场灾难——业界戏称它为 God Activity(上帝 Activity),意思是这个 Activity 什么都管、什么都干。它的问题很具体:改一个按钮的文字,可能不小心碰坏网络请求的逻辑;想给某个页面加单元测试,发现离开 Activity 根本跑不起来;多人协作时大家都改同一个文件,天天冲突。架构模式的出现就是为了把这种大杂烩拆开,让"界面""业务逻辑""数据"各归各位。

Android 项目历史上依次出现过三种主流架构。MVC(Model-View-Controller)是最早的尝试,但在 Android 里"Controller"几乎就是 Activity 本身,结果 Model、View、Controller 三样还是挤在一起,没真正分开。于是有了 MVP(Model-View-Presenter):把业务逻辑抽到一个叫 Presenter 的类里,Activity 只管显示 UI,两者通过一个契约接口(Contract)沟通——这样 Activity 变薄了,Presenter 也能单独测试。再后来 Google 力推 MVVM(Model-View-ViewModel):和 MVP 类似,但 ViewModel 不直接调用 View 的方法,而是把数据放进一个叫 LiveData 的"可观察盒子",UI 自己观察这个盒子,数据一变就自动刷新。这比 MVP 的"Presenter 主动调 View 方法"更省心,也更难写错。

MVVM 里有个特别关键的角色叫 ViewModel,它最大的本事是:屏幕旋转时不会跟着 Activity 一起销毁重建。想象你在填一个长表单,填到一半把手机横过来——如果用 Activity 存数据,旋转后数据全没了,得重填;而用 ViewModel 存数据,旋转后新 Activity 拿到的是同一个 ViewModel,数据原封不动还在。这就是 ViewModel 存在的核心意义。

最后是 Jetpack Navigation,它解决的是"页面之间怎么跳转"的问题。原生 Android 跳页用 Intent,每跳一次要手写目标类名、手动传参数、手动管返回栈,页面一多就乱。Navigation 让你先用一张 XML 导航图把"有哪些页面、页面之间能怎么跳"画出来,然后用一个 NavController 统一调度跳转、传参、返回,还能配合底部导航栏自动高亮当前 Tab。

如果你有前端开发经验,可以用下面的对照表快速建立映射;零基础读者直接进入 第 1 节 即可。

前端开发者速查:架构演进对照

前端架构Android 架构核心思想
jQuery 面条代码MVC(原始)没有分离,全部写在 Activity
Redux / VuexMVP明确分离 View 和 Presenter
React + Hooks / ZustandMVVM数据驱动 UI,响应式

1. MVC 模式(最早期)

老项目(2015年前后)最常见的"伪 MVC",实际上所有逻辑都堆在 Activity 中:

java
// ❌ 典型的"God Activity":既是 Controller 又是 View
public class UserListActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user_list);

        // Model:直接在这里定义数据结构
        // View:直接在这里操作 UI
        // Controller:直接在这里写业务逻辑
        // 全部混在一起!

        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, "加载失败", Toast.LENGTH_SHORT).show();
                });
            }
        }).start();
    }
}

2. MVP 模式(2015-2019 主流)

MVP 是 Android 中最广泛使用的架构模式,老项目中最常见:

View (Activity/Fragment) ←→ Presenter ←→ Model (Repository/API)
         ↑                        ↑
    只负责 UI               业务逻辑和数据处理

2.1 定义契约

java
// 定义 View 和 Presenter 的接口(契约)
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 实现

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;  // 防止内存泄漏
    }

    @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 实现(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);

        // 初始化 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);

        // 创建 Presenter
        presenter = new UserListPresenter(new UserRepository());
        presenter.attachView(this);
        presenter.loadUsers();
    }

    private void onUserClicked(User user) {
        presenter.onUserClicked(user);
    }

    // ---- View 接口实现 ----

    @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();  // 防止泄漏
    }
}

3. MVVM 模式(2019+ 主流,推荐)

MVVM 使用数据驱动 UI,ViewModel 持有数据,View 观察数据变化:

View (Activity/Fragment)  ←观察→  ViewModel  ←→  Repository
        ↑                              ↑
   只负责 UI 和事件            持有 UI 数据,存活于配置变更

3.1 ViewModel

java
public class UserListViewModel extends ViewModel {

    private final UserRepository repository;

    // MutableLiveData:ViewModel 内部可修改
    private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>();
    private final MutableLiveData<List<User>> users = new MutableLiveData<>();
    private final MutableLiveData<String> errorMessage = new MutableLiveData<>();

    // 对外暴露 LiveData(只读)
    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 观察 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);

        // 获取 ViewModel(配置变更时不会重建)
        viewModel = new ViewModelProvider(this).get(UserListViewModel.class);

        // 初始化 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);

        // 观察数据变化
        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();
        });

        // 首次加载
        if (savedInstanceState == null) {
            viewModel.loadUsers();
        }
    }
}

3.3 LiveData 要点

java
// LiveData 的核心特性:
// 1. 生命周期感知:Activity 销毁后不再发送更新,避免崩溃
// 2. 粘性事件:新的观察者注册时会收到最后一次数据
// 3. 自动取消:不需要手动注销观察者

// setValue():必须在主线程调用
liveData.setValue(newValue);

// postValue():可以在后台线程调用
liveData.postValue(newValue);

// 数据转换(类似 JS 的 .map())
LiveData<String> userName = Transformations.map(userLiveData, user -> user.getName());

// 合并多个 LiveData(类似 Promise.all)
MediatorLiveData<Result> mediator = new MediatorLiveData<>();
mediator.addSource(usersLiveData, users -> combine(users, otherData));
mediator.addSource(otherLiveData, other -> combine(users, other));

LiveData 常见误区

java
// ❌ 错误:在 ViewModel 中持有 Activity/Fragment 引用
public class UserViewModel extends ViewModel {
    private Activity activity;  // 内存泄漏!
}

// ✅ 正确:ViewModel 不引用 View,只通过 LiveData 暴露数据

// ❌ 错误:把点击事件也封装成 LiveData(会导致旋转屏幕后重复触发)
public class LoginViewModel extends ViewModel {
    private MutableLiveData<Boolean> loginClick = new MutableLiveData<>();
}

// ✅ 正确:一次性事件用 SingleLiveEvent 或手动回调
public class SingleLiveEvent<T> extends MutableLiveData<T> {
    // 实现:通过 AtomicBoolean 保证只消费一次
}

// ✅ 正确:后台线程更新数据用 postValue
new Thread(() -> {
    List<User> users = repository.loadUsers();
    usersLiveData.postValue(users);  // 子线程安全
}).start();

3.4 Repository 模式

Repository 是 MVVM 中连接 ViewModel 和数据来源(网络、数据库、缓存)的中介:

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. 先返回本地缓存
        // 2. 后台请求网络
        // 3. 网络成功后更新数据库
        // 4. Room 通过 LiveData 自动通知 UI
        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", "刷新用户失败", e);
            }
        }).start();
    }
}

前端对照:Repository 类似前端项目中的 services/api.ts + stores/cache 的合体,负责统一的数据获取和缓存策略。

4. 架构识别指南

接手老项目时,如何判断使用了哪种架构:

特征架构文件命名
Activity 中直接写网络请求和 UI 操作MVC(无架构)只有 Activity
XxxContractXxxPresenter 接口和类MVPUserContract.javaUserPresenter.java
XxxViewModel + LiveDataMVVMUserViewModel.java
有 Dagger/Hilt 依赖注入通常是 MVP 或 MVVMXxxModule.java@Inject
@HiltAndroidApp@InjectMVVM + DIXxxModule.java

5. Jetpack Navigation 组件

5.1 导航图

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="首页">

        <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="详情">

        <argument
            android:name="userId"
            app:argType="integer" />
    </fragment>

    <fragment
        android:id="@+id/settingsFragment"
        android:name="com.example.app.SettingsFragment"
        android:label="设置" />

</navigation>

5.2 在 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 导航操作

java
// 在 Fragment 中导航到其他页面
Navigation.findNavController(view)
    .navigate(R.id.action_home_to_detail);

// 传递参数
Bundle args = new Bundle();
args.putInt("userId", 123);
Navigation.findNavController(view)
    .navigate(R.id.action_home_to_detail, args);

// 在目标 Fragment 中接收参数
int userId = getArguments().getInt("userId");

// 返回上一页
Navigation.findNavController(view).navigateUp();

// 配合底部导航栏
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 类型安全导航

groovy
// 项目级 build.gradle
plugins {
    id 'androidx.navigation.safeargs' version '2.7.6' apply false
}

// 模块级 build.gradle
plugins {
    id 'androidx.navigation.safeargs'
}
java
// 生成的类提供了类型安全的导航
HomeFragmentDirections.ActionHomeToDetail action =
    HomeFragmentDirections.actionHomeToDetail()
        .setUserId(123);
Navigation.findNavController(view).navigate(action);

6. 老项目中常见的架构库

说明识别特征
MoxyMVP 框架@InjectPresenter@BindViewState
MosbyMVP 框架MvpActivityMvpPresenter
ConductorController-basedController 替代 Fragment
Dagger 2依赖注入@Module@Component@Inject
Hilt依赖注入(简化 Dagger)@HiltAndroidApp@AndroidEntryPoint

7. ViewModel 进阶用法

7.1 ViewModel + SavedStateHandle

java
public class UserDetailViewModel extends ViewModel {
    private final SavedStateHandle handle;

    public UserDetailViewModel(SavedStateHandle handle) {
        this.handle = handle;
        // 从导航参数中获取
        int userId = handle.get("userId");
        loadUser(userId);
    }

    // 保存状态(进程被杀后恢复)
    public void saveDraft(String text) {
        handle.set("draft", text);
    }

    public String getDraft() {
        return handle.get("draft");
    }
}

7.2 ViewModel 工厂

java
// 当 ViewModel 构造方法需要参数时
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");
    }
}

// 使用
UserListViewModel viewModel = new ViewModelProvider(this,
    new ViewModelFactory(repository)).get(UserListViewModel.class);

7.3 LifecycleObserver:生命周期感知组件

java
// 让普通类也能感知 Activity/Fragment 生命周期
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();
    }
}

// 在 Activity/Fragment 中注册
getLifecycle().addObserver(new MyLocationListener(locationManager));

前端对照:类似 React 的 useEffect(() => { start(); return () => stop(); }, []),自动在组件挂载/卸载时执行和清理。

前端开发者备忘

前端架构概念Android 对应备注
组件内状态Activity 成员变量最简单的数据保存
Redux StoreViewModel + LiveData数据驱动 UI
useSelector()liveData.observe()观察数据变化
dispatch(action)viewModel.method()触发状态更新
React RouterJetpack Navigation页面导航
useNavigate()NavController.navigate()编程式导航
useParams()getArguments() / Safe Args路由参数
<Outlet />FragmentContainerView嵌套路由容器
Context ProviderViewModelProvider共享数据
zustand / jotaiViewModel + LiveData轻量状态管理

本章小结

回顾一下这一章的要点:

  • God Activity 问题:把联网、UI、业务逻辑全堆在 Activity 里,会导致难维护、难测试、易冲突,这就是引入架构模式的根本原因
  • MVC 在 Android 里基本退化成"全堆 Activity",没真正分开,是最早期的写法
  • MVP 用 Presenter 承载业务逻辑、Activity 只管 UI,两者通过 Contract 接口沟通;记得在 onDestroydetachView 防止内存泄漏
  • MVVM 用 ViewModel + LiveData 实现"数据驱动 UI":ViewModel 把数据放进 LiveData,UI 观察它,数据变了自动刷新,比 MVP 的主动调 View 更省心
  • ViewModel 的核心特性是能在屏幕旋转(配置变更)后存活,新 Activity 拿到的是同一个 ViewModel,表单数据不会丢
  • LiveData 是生命周期感知的setValue 必须在主线程,postValue 可在后台线程;不要在 ViewModel 里持有 Activity 引用,否则泄漏
  • Repository 是 ViewModel 和数据源(网络/数据库)之间的中介,统一管理"先缓存后联网"等策略
  • Jetpack Navigation 用导航图集中描述页面关系,NavController 统一调度跳转、传参、返回栈,配合 BottomNavigationView 还能自动高亮
  • 接手老项目时可以通过文件命名快速判断架构:XxxContract / XxxPresenter 是 MVP,XxxViewModel 是 MVVM

架构和导航都理清后,下一章我们会进入实战排错环节——学习调试、测试和问题排查的常用工具与方法。

下一步

理解了架构模式后,接下来学习 模块 09:调试、测试与问题排查,掌握实用的调试和排错技能。

面向前端开发者的 Java Android 开发教程