架构优化: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();
});
}
}
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();
});
二、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)
三、模块化与组件化
这两个概念经常被混用。我简单区分一下:模块化是按功能拆模块,组件化是按业务拆组件。说白了,模块化是技术维度,组件化是业务维度。
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();
// 初始化登录组件需要的资源
}
}
四、依赖注入: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();
}
}
五、架构选型总结
说了这么多,到底怎么选?我根据自己的经验画了张图,帮你快速决策。
说实话,架构没有银弹。我见过用MVVM但代码写得一塌糊涂的,也见过用MVC但结构清晰的。关键不在于用什么模式,而在于你有没有坚持把代码分好层、把职责分清楚。
嗯,最后说一句:别为了用架构而用架构。如果你的项目就两个页面,用MVC完全没问题。但如果你在做一个长期维护的产品,MVVM + Jetpack + 模块化,这套组合拳打下来,后期你会感谢自己的。