综合实战:MVVM + Jetpack全家桶项目
说实话,做了这么多年Android开发,我见过太多项目从“能跑就行”慢慢变成“改不动了”。
代码耦合、数据流混乱、测试困难……这些问题就像滚雪球,越滚越大。
所以今天这一章,我想跟你聊聊怎么用MVVM + Jetpack全家桶,搭一个真正能打的项目骨架。
不是纸上谈兵,是我在几个千万级用户量的App里反复验证过的方案。
项目架构设计:三层架构的落地
先说说整体分层。我个人习惯把项目切成三层:数据层、领域层、展示层。
你可能会问:“为什么不用两层?多一层不是增加复杂度吗?”
嗯,我刚开始也这么想。直到有一次,业务方要求把网络请求全部换成本地缓存优先……那改起来,真是头皮发麻。
核心原则:每一层只做自己的事,绝不越界。
数据层(Data Layer)
数据层负责跟外界打交道。网络、数据库、文件、传感器……都归它管。
我一般会拆成三个子模块:
- Repository:数据仓库,统一对外暴露数据接口
- DataSource:数据源,分本地和远程两种
- Model:数据模型,包括DTO和Entity
举个例子,用户信息获取:
class UserRepository @Inject constructor(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) {
suspend fun getUser(id: String): Flow<Resource<User>> {
return flow {
// 先发本地缓存
emit(Resource.Loading(localDataSource.getCachedUser(id)))
// 再请求网络
try {
val remoteUser = remoteDataSource.fetchUser(id)
localDataSource.cacheUser(remoteUser)
emit(Resource.Success(remoteUser))
} catch (e: Exception) {
emit(Resource.Error(e))
}
}
}
}
这里有个坑:千万不要在Repository里直接返回网络数据。我曾经这么干过,结果离线功能完全没法做。
领域层(Domain Layer)
领域层是纯Kotlin模块,不依赖任何Android框架。
它只做两件事:
- 定义UseCase(用例)
- 定义业务逻辑
你想想看,如果业务逻辑写在ViewModel里,那换一个UI框架(比如从Compose换成View系统),业务逻辑就得重写。这合理吗?
class GetUserDetailUseCase @Inject constructor(
private val userRepository: UserRepository
) {
suspend operator fun invoke(userId: String): Flow<Resource<UserDetail>> {
return userRepository.getUser(userId).map { resource ->
resource.map { user ->
// 业务转换:把User转成UserDetail
UserDetail(
id = user.id,
displayName = "${user.lastName} ${user.firstName}",
age = calculateAge(user.birthday)
)
}
}
}
}
注意看,UseCase用operator fun invoke,调用起来就像函数一样自然。
展示层(Presentation Layer)
展示层就是ViewModel + UI。ViewModel持有UseCase,UI观察ViewModel的状态。
我习惯用StateFlow来管理UI状态:
@HiltViewModel
class UserDetailViewModel @Inject constructor(
private val getUserDetail: GetUserDetailUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<UserDetail>>(UiState.Loading)
val uiState: StateFlow<UiState<UserDetail>> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
getUserDetail(userId).collect { resource ->
_uiState.value = when (resource) {
is Resource.Success -> UiState.Success(resource.data)
is Resource.Error -> UiState.Error(resource.message)
is Resource.Loading -> UiState.Loading
}
}
}
}
}
Room + Paging3 + WorkManager的协同
这三个组件配合起来,能解决大部分数据持久化和后台任务的问题。
Room + Paging3:分页加载的最佳实践
我记得第一次做列表分页时,自己写了个分页逻辑,结果各种边界条件处理不好。
后来用了Paging3,真香。
@Dao
interface ArticleDao {
@Query("SELECT * FROM articles ORDER BY publishTime DESC")
fun getPagedArticles(): PagingSource<Int, Article>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(articles: List<Article>)
}
// Repository中
fun getArticles(): Flow<PagingData<Article>> {
return Pager(
config = PagingConfig(pageSize = 20, enablePlaceholders = false),
pagingSourceFactory = { articleDao.getPagedArticles() }
).flow
}
这里有个关键点:PagingSource一定要处理好refresh机制。我见过有人忘了实现getRefreshKey,导致下拉刷新后数据错乱。
WorkManager:后台任务的正确姿势
WorkManager最适合做那些“必须完成”的任务,比如上传日志、同步数据。
我曾经踩过一个坑:用Service做定时同步,结果被系统杀得死死的。
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// 执行同步逻辑
syncData()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
}
// 调度任务
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"sync_work",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
注意runAttemptCount,这是WorkManager自带的重试计数。我一般设3次重试,超过就放弃,避免无限重试耗电。
Hilt + Navigation + Compose的集成
这三个东西放一起,开发体验真的爽。
Hilt注入
Hilt让依赖注入变得简单。我只需要在ViewModel上加个注解:
@HiltViewModel
class MainViewModel @Inject constructor(
private val analyticsRepo: AnalyticsRepository
) : ViewModel()
然后在Activity或Fragment里:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
NavHost()
}
}
}
}
Navigation + Compose
Compose的导航比Fragment简单太多了:
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
composable("home") {
HomeScreen(navController)
}
composable(
route = "detail/{articleId}",
arguments = listOf(navArgument("articleId") { type = NavType.StringType })
) { backStackEntry ->
val articleId = backStackEntry.arguments?.getString("articleId") ?: ""
DetailScreen(articleId)
}
}
}
你可能会问:“参数传递怎么办?”
用SavedStateHandle,Hilt会自动注入:
@HiltViewModel
class DetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val getArticleDetail: GetArticleDetailUseCase
) : ViewModel() {
private val articleId: String = savedStateHandle["articleId"] ?: ""
}
性能监控与Crash分析
项目上线后,最怕的就是用户反馈“闪退了”但自己复现不了。
性能监控
我一般用Firebase Performance + 自定义Trace:
class PerformanceMonitor {
fun traceNetworkRequest(url: String, block: suspend () -> Unit) {
val trace = FirebasePerformance.getInstance().newTrace("network_$url")
trace.start()
try {
block()
} finally {
trace.stop()
}
}
}
但要注意,不要每个方法都加Trace。我见过有人给getter都加Trace,结果性能监控本身成了性能瓶颈。
Crash分析
除了Firebase Crashlytics,我还会在本地存一份Crash日志:
class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, throwable: Throwable) {
val crashInfo = buildString {
appendLine("Time: ${System.currentTimeMillis()}")
appendLine("Thread: ${thread.name}")
appendLine("Exception: ${throwable.message}")
throwable.stackTrace.forEach { appendLine(it.toString()) }
}
// 写入本地文件
context.openFileOutput("crash_log.txt", Context.MODE_APPEND).use {
it.write(crashInfo.toByteArray())
}
// 上报到服务器
reportCrash(crashInfo)
}
}
这个Handler要在Application的onCreate里尽早注册:
Thread.setDefaultUncaughtExceptionHandler(CrashHandler(this))
小技巧:Crash日志最好加上设备信息、App版本、用户ID。这样排查问题时能快速定位到具体环境。
注意:自定义CrashHandler不能替代Crashlytics。它只是兜底方案,防止Crashlytics上报失败时丢失数据。
知识体系总览
下面这张图,是我整理的这个章节的核心逻辑。你可以把它当作项目架构的“地图”:
这张图把整个架构串起来了。你从上往下看,就是一次完整的数据请求流程。
从展示层发起,经过领域层处理业务,再到数据层获取数据,最后原路返回。
基础设施层(Hilt、性能监控、Crash分析)贯穿始终,为所有层提供支撑。
好了,这一章的内容就到这里。项目架构搭好了,后面每一章都会在这个骨架上填充血肉。
记住:好的架构不是设计出来的,是演进出来的。别追求一步到位,先跑起来,再优化。
公众号:蓝海资料掘金营,微信deep3321