作为一名在Android开发领域摸爬滚打多年的老兵,我见证了Android架构从最初的混沌状态到如今的成熟体系。记得2013年我刚入行时,项目里所有的代码都堆在Activity里,一个类动辄几千行,改一处功能就像拆炸弹一样提心吊胆。如今回看这段历史,架构的演进本质上是我们对软件工程认知的不断深化。
架构设计不是炫技,而是为了解决实际问题。当你的Activity超过3000行代码时,当每次需求变更都要修改十几处分散的逻辑时,当测试同学抱怨无法编写单元测试时——这些痛点推动着我们不断探索更好的代码组织方式。本文将带你走过这段演进历程,并分享我在多个大型项目中验证过的实战经验。
早期的Android项目大多处于这种状态,所有代码都写在Activity或Fragment中。我曾接手过一个电商项目的主页Activity,足足有4800行代码!里面混杂着UI渲染、网络请求、数据库操作、业务逻辑,甚至还有分享和推送的处理。
这种架构最致命的问题是生命周期管理。记得有一次用户反馈在旋转屏幕后购物车数据丢失,排查发现是因为开发者在onCreate里初始化数据却没有处理配置变更。这种问题在无架构项目中比比皆是。
经验之谈:如果你现在还在写这样的代码,请立即停止!即使是最简单的MVC也比无架构要好十倍。
MVC(Model-View-Controller)是第一个被广泛采用的架构模式。理论上它很美好:Model处理数据,View负责显示,Controller作为中间人。但在Android中,这个模式常常变形为"MCV"——因为Activity既承担了Controller的角色,又通过findViewById直接操作View。
我在2015年参与的一个新闻客户端就采用了MVC架构。虽然比无架构时代进步了,但我们很快发现了问题:
kotlin复制// 典型的Android MVC代码示例
class ArticleActivity : AppCompatActivity() {
// Controller和View的混合体
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_article)
// 直接操作View
findViewById<TextView>(R.id.title).text = "Loading..."
// 包含业务逻辑
loadArticle()
}
private fun loadArticle() {
// 直接处理网络请求
RetrofitClient.api.getArticle(id).enqueue(object : Callback<Article> {
override fun onResponse(call: Call<Article>, response: Response<Article>) {
// 直接更新UI
findViewById<TextView>(R.id.title).text = response.body()?.title
// 处理业务逻辑
updateReadCount(response.body()?.id)
}
})
}
}
MVP(Model-View-Presenter)的出现解决了MVC的核心痛点。我参与开发的一款金融APP在2017年迁移到MVP架构后,代码可维护性显著提升。关键改进在于:
kotlin复制// 改进后的MVP实现
class ArticlePresenter(
private val view: ArticleContract.View,
private val repository: ArticleRepository
) {
fun loadArticle(id: String) {
repository.getArticle(id)
.subscribe({ article ->
view.showArticle(article)
repository.updateReadCount(article.id)
}, { error ->
view.showError(error.message)
})
}
}
class ArticleActivity : AppCompatActivity(), ArticleContract.View {
private lateinit var presenter: ArticlePresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_article)
presenter = ArticlePresenter(this, ArticleRepositoryImpl())
presenter.loadArticle(intent.getStringExtra("id")!!)
}
override fun showArticle(article: Article) {
findViewById<TextView>(R.id.title).text = article.title
}
}
但MVP也有其局限性。在开发一个社交应用时,我们遇到了Presenter生命周期管理的问题。用户快速切换页面会导致前一个Presenter仍然持有View引用,造成内存泄漏。解决方案是引入RxJava的disposable或使用Android Architecture Components中的ViewModel。
MVVM(Model-View-ViewModel)结合LiveData和DataBinding,彻底改变了Android开发的范式。我在当前公司主导的电商项目全面采用MVVM,配合Kotlin协程,开发效率提升了40%以上。
MVVM的核心优势:
kotlin复制class ArticleViewModel(
private val repository: ArticleRepository
) : ViewModel() {
private val _article = MutableLiveData<Article>()
val article: LiveData<Article> = _article
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
fun loadArticle(id: String) {
viewModelScope.launch {
try {
_article.value = repository.getArticle(id)
repository.updateReadCount(id)
} catch (e: Exception) {
_error.value = e.message
}
}
}
}
class ArticleActivity : AppCompatActivity() {
private val viewModel by viewModels<ArticleViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_article)
viewModel.article.observe(this) { article ->
findViewById<TextView>(R.id.title).text = article.title
}
viewModel.error.observe(this) { error ->
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
viewModel.loadArticle(intent.getStringExtra("id")!!)
}
}
实战技巧:ViewModel应该保持纯净,不要导入任何Android框架的类(如Context)。这样可以使业务逻辑更容易测试和复用。
当我们的电商APP发展到有上百个状态需要管理时,MVVM开始显得力不从心。这时我们引入了MVI(Model-View-Intent)架构,特别适合以下场景:
MVI的核心概念:
kotlin复制// 电商商品详情页的MVI实现
data class ProductState(
val product: Product? = null,
val loading: Boolean = false,
val error: String? = null,
val selectedColor: String? = null,
val selectedSize: String? = null,
val quantity: Int = 1,
val isFavorite: Boolean = false
)
sealed class ProductIntent {
object Load : ProductIntent()
data class SelectColor(val color: String) : ProductIntent()
data class SelectSize(val size: String) : ProductIntent()
object ToggleFavorite : ProductIntent()
}
class ProductViewModel(
private val repository: ProductRepository
) : ViewModel() {
private val _state = MutableStateFlow(ProductState())
val state: StateFlow<ProductState> = _state
fun processIntent(intent: ProductIntent) {
when (intent) {
is ProductIntent.Load -> loadProduct()
is ProductIntent.SelectColor -> selectColor(intent.color)
is ProductIntent.SelectSize -> selectSize(intent.size)
is ProductIntent.ToggleFavorite -> toggleFavorite()
}
}
private fun loadProduct() {
_state.update { it.copy(loading = true) }
viewModelScope.launch {
try {
val product = repository.getProduct()
_state.update {
it.copy(
loading = false,
product = product,
isFavorite = repository.isFavorite(product.id)
)
}
} catch (e: Exception) {
_state.update { it.copy(loading = false, error = e.message) }
}
}
}
private fun selectColor(color: String) {
_state.update { it.copy(selectedColor = color) }
}
private fun selectSize(size: String) {
_state.update { it.copy(selectedSize = size) }
}
private fun toggleFavorite() {
_state.update { state ->
val productId = state.product?.id ?: return@update state
val newFavorite = !state.isFavorite
viewModelScope.launch {
repository.setFavorite(productId, newFavorite)
}
state.copy(isFavorite = newFavorite)
}
}
}
MVI的最大优势是状态的可预测性。在任何时候,你都可以通过打印当前State来了解UI的完整状态,这在调试复杂交互时非常有用。
在大型项目中,我通常采用三层架构:
kotlin复制// 典型的三层依赖关系
// UI层 → Domain层 → Data层
// Data层
interface ProductRepository {
suspend fun getProduct(id: String): Product
}
// Domain层
class GetProductUseCase(
private val repository: ProductRepository
) {
suspend operator fun invoke(id: String): Product {
return repository.getProduct(id)
}
}
// UI层
class ProductViewModel(
private val getProductUseCase: GetProductUseCase
) : ViewModel() {
// ...
}
经验之谈:Domain层应该是纯Kotlin/Java模块,不依赖任何Android框架。这样可以在不修改业务逻辑的情况下替换UI或数据层。
在电商项目中,我们曾因为商品数据在多处缓存导致状态不一致。引入SSOT原则后:
kotlin复制class ProductRepositoryImpl(
private val local: ProductLocalDataSource,
private val remote: ProductRemoteDataSource
) : ProductRepository {
override suspend fun getProduct(id: String): Product {
// 先从本地获取
val localProduct = local.getProduct(id)
if (localProduct != null) {
return localProduct
}
// 本地没有则从网络获取
val remoteProduct = remote.getProduct(id)
local.saveProduct(remoteProduct)
return remoteProduct
}
}
在即时通讯应用中,我们使用UDF处理消息状态:
mermaid复制graph LR
A[用户操作] --> B[Intent]
B --> C[ViewModel处理]
C --> D[更新State]
D --> E[UI渲染]
这种模式使复杂的消息状态管理变得清晰可控。
对于大型项目,我推荐按功能划分模块:
code复制app/
├─ features/
│ ├─ home/
│ │ ├─ presentation/ # UI层
│ │ ├─ domain/ # 业务逻辑
│ │ └─ data/ # 数据源
│ └─ product/
│ ├─ presentation/
│ ├─ domain/
│ └─ data/
├─ core/
│ ├─ network/
│ ├─ database/
│ └─ common/
└─ app/ # 主模块
每个feature模块可以独立编译,大幅提升构建速度。在我们的项目中,全量构建时间从8分钟减少到1分钟。
使用Hilt实现依赖注入:
kotlin复制// Data层模块
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
@Singleton
fun provideProductRepository(
local: ProductLocalDataSource,
remote: ProductRemoteDataSource
): ProductRepository {
return ProductRepositoryImpl(local, remote)
}
}
// Domain层用例
class GetProductUseCase @Inject constructor(
private val repository: ProductRepository
) { ... }
// UI层ViewModel
@HiltViewModel
class ProductViewModel @Inject constructor(
private val getProductUseCase: GetProductUseCase
) : ViewModel() { ... }
完整的测试金字塔:
kotlin复制class GetProductUseCaseTest {
private lateinit var useCase: GetProductUseCase
private val mockRepository = mockk<ProductRepository>()
@Before
fun setup() {
useCase = GetProductUseCase(mockRepository)
}
@Test
fun `get product should return from repository`() = runTest {
val expectedProduct = Product("1", "Phone", 999.0)
coEvery { mockRepository.getProduct("1") } returns expectedProduct
val result = useCase("1")
assertEquals(expectedProduct, result)
coVerify { mockRepository.getProduct("1") }
}
}
| 类别 | 推荐方案 |
|---|---|
| 语言 | Kotlin + KSP |
| 异步 | Coroutines + Flow |
| DI | Hilt |
| UI | Jetpack Compose |
| 导航 | Compose Navigation |
| 网络 | Retrofit + OkHttp + Moshi |
| 本地存储 | Room + DataStore |
| 日志 | Timber |
| 构建工具 | Gradle KTS + Version Catalogs |
项目规模:
团队经验:
状态复杂度:
在社交APP中,我们通过以下方式优化Room性能:
kotlin复制@Dao
interface MessageDao {
@Query("SELECT * FROM messages WHERE conversationId = :conversationId ORDER BY timestamp DESC")
fun getMessages(conversationId: String): PagingSource<Int, Message>
@Query("SELECT * FROM messages WHERE id = :messageId")
suspend fun getMessage(messageId: String): Message?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(messages: List<Message>)
}
电商APP的优化实践:
kotlin复制class CacheInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.method != "GET") {
return chain.proceed(request)
}
// 强制缓存有效期为5分钟
val modifiedRequest = request.newBuilder()
.header("Cache-Control", "public, max-age=300")
.build()
return chain.proceed(modifiedRequest)
}
}
问题:把所有逻辑都塞进ViewModel,导致"巨型ViewModel"
解决方案:
问题:XML中嵌入复杂逻辑,难以调试
解决方案:
问题:在非UI线程更新LiveData导致崩溃
解决方案:
kotlin复制class SafeCounterViewModel : ViewModel() {
private val _count = MutableLiveData<Int>()
val count: LiveData<Int> = _count
private val counter = AtomicInteger(0)
fun increment() {
viewModelScope.launch(Dispatchers.Default) {
val newValue = counter.incrementAndGet()
_count.postValue(newValue)
}
}
}
对于遗留项目,建议的演进路径:
过渡期可以采用混合架构:
Compose与MVI架构是天作之合。状态变化驱动UI重组的概念与MVI的单向数据流完美契合。
kotlin复制@Composable
fun ProductScreen(viewModel: ProductViewModel) {
val state by viewModel.state.collectAsState()
when {
state.loading -> LoadingIndicator()
state.error != null -> ErrorMessage(state.error!!)
else -> ProductContent(
product = state.product!!,
onColorSelected = { viewModel.processIntent(ProductIntent.SelectColor(it)) },
onFavoriteClicked = { viewModel.processIntent(ProductIntent.ToggleFavorite) }
)
}
}
随着Kotlin跨平台能力的增强,我们可以将Domain层甚至部分Data层共享给iOS端。这对架构设计提出了新要求:
在经历了数十个Android项目后,我的架构设计哲学可以总结为:
一个令我印象深刻的反例是:某金融APP为了追求"最新架构",在团队没有MVI经验的情况下强行采用,结果开发效率大幅下降,最终不得不回退到MVVM。这告诉我们:架构选型必须考虑团队实际情况。
最后分享一个实用技巧:在项目初期,可以建立一个"架构决策记录"(ADR)文档,记录每个架构决策的背景、考虑因素和预期影响。这在新成员加入或回顾项目时非常有价值。