架构演进:从混沌到清晰
Android应用架构经历了从Activity/Fragment直接操作一切,到MVC、MVP、MVVM,再到现代MVI和Clean Architecture的演进。每一次演进都是为了解决特定的问题:代码耦合、测试困难、状态管理混乱。
架构演进的核心驱动力
- 关注点分离:UI、业务逻辑、数据访问层职责清晰
- 可测试性:业务逻辑独立于Android框架,便于单元测试
- 可维护性:代码结构清晰,新人上手快
- 生命周期感知:正确处理配置变更和生命周期事件
MVP:分离视图与逻辑
MVP架构设计
MVP(Model-View-Presenter)将业务逻辑从Activity/Fragment中抽离,Presenter作为中间层协调View和Model:
// Contract:定义View和Presenter接口
interface LoginContract {
interface View {
fun showLoading()
fun hideLoading()
fun showError(message: String)
fun navigateToHome()
}
interface Presenter {
fun attachView(view: View)
fun detachView()
fun login(username: String, password: String)
}
}
// Presenter实现
class LoginPresenter(
private val userRepository: UserRepository
) : LoginContract.Presenter {
private var view: LoginContract.View? = null
private val scope = CoroutineScope(Dispatchers.Main + Job())
override fun attachView(view: LoginContract.View) {
this.view = view
}
override fun detachView() {
view = null
scope.cancel() // 防止内存泄漏
}
override fun login(username: String, password: String) {
view?.showLoading()
scope.launch {
try {
val result = withContext(Dispatchers.IO) {
userRepository.login(username, password)
}
when (result) {
is Result.Success -> view?.navigateToHome()
is Result.Error -> view?.showError(result.message)
}
} finally {
view?.hideLoading()
}
}
}
}
// Activity实现View接口
class LoginActivity : AppCompatActivity(), LoginContract.View {
private lateinit var presenter: LoginContract.Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
// 依赖注入(实际项目中使用Dagger/Hilt)
presenter = LoginPresenter(UserRepositoryImpl())
presenter.attachView(this)
loginButton.setOnClickListener {
presenter.login(usernameEdit.text.toString(),
passwordEdit.text.toString())
}
}
override fun onDestroy() {
super.onDestroy()
presenter.detachView() // 关键:防止内存泄漏
}
override fun showLoading() { progressBar.visibility = View.VISIBLE }
override fun hideLoading() { progressBar.visibility = View.GONE }
override fun showError(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
override fun navigateToHome() {
startActivity(Intent(this, HomeActivity::class.java))
finish()
}
}
MVP设计要点
- Presenter持有View的弱引用或手动管理生命周期
- View接口只定义UI操作,不包含业务逻辑
- Presenter可单元测试,不依赖Android框架
MVVM:响应式数据绑定
Jetpack ViewModel + LiveData
MVVM通过数据绑定实现View和ViewModel的自动同步,结合Jetpack组件实现生命周期感知:
// ViewModel:业务逻辑持有者
class LoginViewModel(
private val userRepository: UserRepository
) : ViewModel() {
// LiveData:可观察的数据持有者
private val _loginState = MutableLiveData()
val loginState: LiveData = _loginState
private val _isLoading = MutableLiveData()
val isLoading: LiveData = _isLoading
fun login(username: String, password: String) {
if (!validateInput(username, password)) {
_loginState.value = LoginState.Error("Invalid input")
return
}
_isLoading.value = true
viewModelScope.launch {
try {
val result = userRepository.login(username, password)
_loginState.value = when (result) {
is Result.Success -> LoginState.Success
is Result.Error -> LoginState.Error(result.message)
}
} finally {
_isLoading.value = false
}
}
}
private fun validateInput(username: String, password: String): Boolean {
return username.isNotBlank() && password.length >= 6
}
// 密封类表示状态
sealed class LoginState {
object Success : LoginState()
data class Error(val message: String) : LoginState()
}
}
// Activity:观察数据变化,自动更新UI
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels {
LoginViewModelFactory(UserRepositoryImpl())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
// 观察Loading状态
viewModel.isLoading.observe(this) { isLoading ->
progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
loginButton.isEnabled = !isLoading
}
// 观察登录状态
viewModel.loginState.observe(this) { state ->
when (state) {
is LoginViewModel.LoginState.Success -> navigateToHome()
is LoginViewModel.LoginState.Error -> showError(state.message)
}
}
loginButton.setOnClickListener {
viewModel.login(
usernameEdit.text.toString(),
passwordEdit.text.toString()
)
}
}
}
StateFlow替代LiveData
Kotlin协程的StateFlow提供了更灵活的响应式编程方式:
class ModernLoginViewModel(
private val userRepository: UserRepository
) : ViewModel() {
// StateFlow:LiveData的替代方案,支持多次观察
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow = _uiState.asStateFlow()
fun login(username: String, password: String) {
_uiState.update { it.copy(isLoading = true) }
viewModelScope.launch {
userRepository.login(username, password)
.onSuccess { user ->
_uiState.update {
it.copy(isLoading = false, user = user, error = null)
}
}
.onFailure { exception ->
_uiState.update {
it.copy(isLoading = false, error = exception.message)
}
}
}
}
data class LoginUiState(
val isLoading: Boolean = false,
val user: User? = null,
val error: String? = null
)
}
// Activity中使用
class ModernLoginActivity : AppCompatActivity() {
private val viewModel: ModernLoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 收集StateFlow(lifecycle-aware)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUi(state)
}
}
}
}
private fun updateUi(state: ModernLoginViewModel.LoginUiState) {
progressBar.isVisible = state.isLoading
state.error?.let { showError(it) }
state.user?.let { navigateToHome() }
}
}
MVI:单向数据流
MVI架构设计
MVI(Model-View-Intent)强调单向数据流和不可变状态,是MVVM的进一步演进:
// MVI核心组件
// 1. State:不可变的UI状态
data class NewsUiState(
val isLoading: Boolean = false,
val news: List = emptyList(),
val error: String? = null,
val selectedCategory: String = "all"
)
// 2. Intent:用户意图/事件
sealed class NewsIntent {
object LoadNews : NewsIntent()
data class SelectCategory(val category: String) : NewsIntent()
data class RefreshNews(val forceRefresh: Boolean) : NewsIntent()
data class NewsClicked(val newsId: String) : NewsIntent()
}
// 3. Effect:一次性事件(如导航、Toast)
sealed class NewsEffect {
data class NavigateToDetail(val newsId: String) : NewsEffect()
data class ShowToast(val message: String) : NewsEffect()
}
// 4. ViewModel:处理Intent,产生新State
class NewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
private val _state = MutableStateFlow(NewsUiState())
val state: StateFlow = _state.asStateFlow()
private val _effect = Channel()
val effect: Flow = _effect.receiveAsFlow()
fun processIntent(intent: NewsIntent) {
when (intent) {
is NewsIntent.LoadNews -> loadNews()
is NewsIntent.SelectCategory -> selectCategory(intent.category)
is NewsIntent.RefreshNews -> refreshNews(intent.forceRefresh)
is NewsIntent.NewsClicked -> handleNewsClick(intent.newsId)
}
}
private fun loadNews() {
_state.update { it.copy(isLoading = true) }
viewModelScope.launch {
newsRepository.getNews(_state.value.selectedCategory)
.onSuccess { news ->
_state.update {
it.copy(isLoading = false, news = news, error = null)
}
}
.onFailure { error ->
_state.update {
it.copy(isLoading = false, error = error.message)
}
}
}
}
private fun handleNewsClick(newsId: String) {
viewModelScope.launch {
_effect.send(NewsEffect.NavigateToDetail(newsId))
}
}
// ... 其他处理方法
}
// Activity:发送Intent,观察State和Effect
class NewsActivity : AppCompatActivity() {
private val viewModel: NewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_news)
// 观察State
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { state ->
render(state)
}
}
}
// 观察Effect
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.effect.collect { effect ->
handleEffect(effect)
}
}
}
// 发送Intent
swipeRefresh.setOnRefreshListener {
viewModel.processIntent(NewsIntent.RefreshNews(forceRefresh = true))
}
categoryChipGroup.setOnCheckedChangeListener { _, checkedId ->
val category = when (checkedId) {
R.id.chipTech -> "tech"
R.id.chipSports -> "sports"
else -> "all"
}
viewModel.processIntent(NewsIntent.SelectCategory(category))
}
// 初始加载
viewModel.processIntent(NewsIntent.LoadNews)
}
private fun render(state: NewsUiState) {
swipeRefresh.isRefreshing = state.isLoading
newsAdapter.submitList(state.news)
state.error?.let { showError(it) }
}
private fun handleEffect(effect: NewsEffect) {
when (effect) {
is NewsEffect.NavigateToDetail -> {
startActivity(NewsDetailActivity.newIntent(this, effect.newsId))
}
is NewsEffect.ShowToast -> {
Toast.makeText(this, effect.message, Toast.LENGTH_SHORT).show()
}
}
}
}
MVI的优势
- 可预测性:单一数据源,状态变化可追踪
- 可调试性:每个状态变化都有对应的Intent
- 时间旅行调试:可以记录和回放状态变化
- 测试友好:Intent -> State的纯函数易于测试
Clean Architecture:分层架构
分层设计原则
// Clean Architecture分层
// 1. Domain层(最内层,不依赖任何框架)
// 2. Data层(数据访问,依赖Domain)
// 3. Presentation层(UI,依赖Domain)
// Domain层:实体和用例
// domain/model/User.kt
data class User(
val id: String,
val name: String,
val email: String
)
// domain/repository/UserRepository.kt
interface UserRepository {
suspend fun getUser(id: String): Result
suspend fun saveUser(user: User): Result
}
// domain/usecase/GetUserUseCase.kt
class GetUserUseCase(private val userRepository: UserRepository) {
suspend operator fun invoke(userId: String): Result {
return userRepository.getUser(userId)
}
}
// Data层:Repository实现
// data/repository/UserRepositoryImpl.kt
class UserRepositoryImpl(
private val userDao: UserDao,
private val userApi: UserApi
) : UserRepository {
override suspend fun getUser(id: String): Result {
return try {
// 优先从本地获取
val localUser = userDao.getUser(id)
if (localUser != null) {
return Result.success(localUser.toDomainModel())
}
// 从网络获取并缓存
val remoteUser = userApi.getUser(id)
userDao.insertUser(remoteUser.toEntity())
Result.success(remoteUser.toDomainModel())
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun saveUser(user: User): Result {
// 实现保存逻辑
return Result.success(Unit)
}
}
// Presentation层:ViewModel
// presentation/viewmodel/UserViewModel.kt
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _user = MutableStateFlow(null)
val user: StateFlow = _user.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
getUserUseCase(userId)
.onSuccess { _user.value = it }
.onFailure { /* 处理错误 */ }
}
}
}
依赖规则
- 内层不依赖外层
- 依赖方向:Presentation -> Domain <- Data
- 通过接口实现依赖倒置
- 便于单元测试和模块替换
架构选型决策树
| 架构 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| MVP | 中小型项目,需要清晰分离 | 职责清晰,可测试 | 接口膨胀,样板代码多 |
| MVVM | 大多数现代Android项目 | 生命周期感知,数据驱动 | 数据绑定调试困难 |
| MVI | 复杂状态管理,团队协作 | 单向数据流,可预测 | 学习曲线陡峭 |
| Clean Architecture | 大型项目,长期维护 | 高度解耦,可测试 | 过度设计风险 |
架构设计原则
- 不要过度设计:简单项目用简单架构
- 保持一致性:团队统一架构规范
- 可测试性优先:架构应便于单元测试
- 渐进式演进:随项目发展调整架构
总结
Android架构演进的核心是从混乱到有序,从紧耦合到高内聚低耦合。没有最好的架构,只有最适合的架构。理解每种架构的设计思想和适用场景,根据项目规模、团队能力和业务复杂度做出合理选择,是Android架构师的核心能力。