架构优化:MVC/MVP/MVVM对比、Jetpack组件最佳实践、模块化与组件化、依赖注入(Hilt)

架构这个话题,说实话,是Android开发里最容易被忽视但又最致命的一环。我见过太多项目,前期图快,Activity里塞了几千行代码,结果半年后没人敢动。今天咱们就把架构这件事聊透。

一、MVC / MVP / MVVM:三种架构的实战对比

这三种模式,说白了就是怎么把代码分家。我最早做Android时用的就是MVC,后来被MVP折磨过,现在基本固定在MVVM。咱们一个个看。

1. MVC:最原始的分工

Model-View-Controller。Android里Activity既是View又是Controller,这就出问题了。

// MVC 典型写法:Activity 又管界面又管逻辑
public class LoginActivity extends AppCompatActivity {
    private EditText etUsername, etPassword;
    private Button btnLogin;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        
        // View 初始化
        etUsername = findViewById(R.id.et_username);
        etPassword = findViewById(R.id.et_password);
        btnLogin = findViewById(R.id.btn_login);
        
        // Controller 逻辑直接写在 Activity 里
        btnLogin.setOnClickListener(v -> {
            String username = etUsername.getText().toString();
            String password = etPassword.getText().toString();
            
            // 这里直接做网络请求
            new Thread(() -> {
                // 网络请求...
                runOnUiThread(() -> {
                    // 更新UI
                    Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
                });
            }).start();
        });
    }
}
我曾经踩过的坑:一个Activity写了2000多行,改一个登录逻辑要翻半天。测试?根本没法写单元测试,因为逻辑和Android生命周期绑死了。

2. MVP:把逻辑抽出来

MVP引入了Presenter层,View只负责UI操作,逻辑全在Presenter里。但问题来了——接口太多了。

// View 接口
public interface LoginView {
    void showLoading();
    void hideLoading();
    void onLoginSuccess();
    void onLoginError(String msg);
}

// Presenter
public class LoginPresenter {
    private LoginView view;
    private LoginModel model;
    
    public void login(String username, String password) {
        view.showLoading();
        model.login(username, password, new Callback() {
            @Override
            public void onSuccess() {
                view.hideLoading();
                view.onLoginSuccess();
            }
            
            @Override
            public void onError(String msg) {
                view.hideLoading();
                view.onLoginError(msg);
            }
        });
    }
}

MVP的好处是逻辑可测试了。但坏处也明显——每个页面要写View接口、Presenter接口、实现类,代码量翻倍。我有个项目用了MVP,光接口文件就40多个,维护起来也挺头疼。

3. MVVM:用数据驱动UI

MVVM用ViewModel + LiveData/StateFlow,View观察数据变化,自动更新。这是我现在最常用的方式。

// ViewModel
public class LoginViewModel extends ViewModel {
    private MutableLiveData<Boolean> _loading = new MutableLiveData<>(false);
    public LiveData<Boolean> loading = _loading;
    
    private MutableLiveData<String> _error = new MutableLiveData<>();
    public LiveData<String> error = _error;
    
    public void login(String username, String password) {
        _loading.setValue(true);
        // 网络请求...
        _loading.setValue(false);
        _error.setValue("登录失败");
    }
}

// Activity 中观察
viewModel.loading.observe(this, isLoading -> {
    if (isLoading) showLoading();
    else hideLoading();
});
我的建议:新项目直接上MVVM。如果你还在用MVC,赶紧重构。MVP可以作为过渡,但别长期用。

二、Jetpack组件最佳实践

Jetpack不是一堆库的堆砌,而是一套架构指南。我见过有人把LiveData当EventBus用,那还不如不用。下面说几个核心组件的正确用法。

1. ViewModel + LiveData:别滥用

LiveData适合做UI状态管理,但不适合做事件。比如弹Toast、跳转页面,用LiveData会有粘性事件问题。

// 错误用法:用 LiveData 发事件
viewModel.toastMsg.observe(this, msg -> {
    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    // 屏幕旋转后,这个 Toast 会再弹一次!
});

// 正确做法:用 Event 包装类
public class Event<T> {
    private boolean handled = false;
    private T content;
    
    public T getContentIfNotHandled() {
        if (handled) return null;
        handled = true;
        return content;
    }
}

2. Room:别写原生SQL

Room帮我们省了写SQLiteHelper的功夫,但很多人还是喜欢写原生SQL。我个人习惯能用注解就用注解,可读性高很多。

@Dao
public interface UserDao {
    @Query("SELECT * FROM users WHERE age > :minAge")
    LiveData<List<User>> getUsersOlderThan(int minAge);
    
    @Insert
    void insert(User user);
    
    @Delete
    void delete(User user);
}

3. Navigation:单Activity架构

我强烈推荐单Activity + Navigation。以前用FragmentTransaction手动管理,回退栈经常出bug。用了Navigation后,这些问题基本没了。

// nav_graph.xml 中定义
<fragment
    android:id="@+id/loginFragment"
    android:name=".ui.login.LoginFragment"
    android:label="登录">
    <action
        android:id="@+id/action_login_to_home"
        app:destination="@id/homeFragment" />
</fragment>

// 代码中跳转
findNavController().navigate(R.id.action_login_to_home)
小技巧:Jetpack组件之间是互相配合的。比如ViewModel + LiveData + Room + Navigation,一套下来代码量能减少30%以上。

三、模块化与组件化

这两个概念经常被混用。我简单区分一下:模块化是按功能拆模块,组件化是按业务拆组件。说白了,模块化是技术维度,组件化是业务维度。

1. 模块化怎么做

我习惯把项目拆成三层:

  • 基础层:网络库、图片加载、工具类
  • 功能层:登录模块、支付模块、推送模块
  • 业务层:首页、详情页、个人中心
// 模块化项目结构
app/
├── lib_base/          // 基础层:网络、图片、工具
├── lib_network/       // 网络模块
├── lib_image/         // 图片加载模块
├── module_login/      // 登录功能
├── module_pay/        // 支付功能
└── app/               // 主工程,组装所有模块

2. 组件化:独立运行与集成

组件化比模块化更进一步。每个业务组件可以独立运行、独立测试。我做过一个项目,首页、搜索、个人中心三个组件,各自有独立的Application。

// 组件化配置:每个组件有自己的 build.gradle
if (isModule.toBoolean()) {
    // 组件模式:独立运行
    apply plugin: 'com.android.application'
} else {
    // 集成模式:作为库
    apply plugin: 'com.android.library'
}

// 组件中声明独立 Application
public class LoginApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化登录组件需要的资源
    }
}
我曾经踩过的坑:组件化后,组件之间的通信成了大问题。后来用了ARouter,但路由表维护起来也挺麻烦。建议小团队别一上来就搞组件化,模块化就够了。

四、依赖注入:Hilt实战

依赖注入说白了就是帮你new对象。以前用Dagger2,配置太复杂了。Hilt是Dagger的简化版,配合Jetpack用起来很顺手。

1. Hilt的基本用法

// Application 上声明
@HiltAndroidApp
public class MyApp extends Application {}

// Activity 上声明
@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
    @Inject
    LoginViewModel viewModel;  // Hilt 自动注入
}

// ViewModel 用 Hilt 创建
@HiltViewModel
public class LoginViewModel extends ViewModel {
    private final LoginRepository repository;
    
    @Inject
    public LoginViewModel(LoginRepository repository) {
        this.repository = repository;
    }
}

2. 模块化中的Hilt

每个模块可以有自己的Hilt Module,提供该模块需要的依赖。

@Module
@InstallIn(SingletonComponent.class)
public class NetworkModule {
    @Provides
    @Singleton
    public OkHttpClient provideOkHttpClient() {
        return new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .build();
    }
    
    @Provides
    @Singleton
    public Retrofit provideRetrofit(OkHttpClient client) {
        return new Retrofit.Builder()
                .baseUrl("https://api.example.com/")
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    }
}
我的习惯:Hilt的Module不要写太多。如果一个Module超过20行,说明你的依赖太复杂了,该考虑重构了。

五、架构选型总结

说了这么多,到底怎么选?我根据自己的经验画了张图,帮你快速决策。

Android架构选型决策图 项目规模 小型项目(1-3人) 中大型项目(3人+) MVVM + Jetpack 模块化(按功能拆) MVVM + Jetpack + Hilt 组件化(按业务拆) 快速开发,维护成本低 高扩展性,团队协作好

说实话,架构没有银弹。我见过用MVVM但代码写得一塌糊涂的,也见过用MVC但结构清晰的。关键不在于用什么模式,而在于你有没有坚持把代码分好层、把职责分清楚。

嗯,最后说一句:别为了用架构而用架构。如果你的项目就两个页面,用MVC完全没问题。但如果你在做一个长期维护的产品,MVVM + Jetpack + 模块化,这套组合拳打下来,后期你会感谢自己的。


专注资料整理