综合实战: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上报失败时丢失数据。

知识体系总览

下面这张图,是我整理的这个章节的核心逻辑。你可以把它当作项目架构的“地图”:

MVVM + Jetpack全家桶 项目架构总览 展示层 Compose UI ViewModel + StateFlow Navigation 路由 领域层 UseCase 用例 业务逻辑转换 纯Kotlin模块 数据层 Repository 仓库 Room + Paging3 WorkManager 后台 基础设施 Hilt 依赖注入 | Firebase 性能监控 | Crashlytics 崩溃分析 数据流向:UI事件 -> ViewModel -> UseCase -> Repository -> DataSource 数据返回:DataSource -> Repository -> UseCase -> ViewModel -> UI Room Paging3 WorkManager Hilt

这张图把整个架构串起来了。你从上往下看,就是一次完整的数据请求流程。

从展示层发起,经过领域层处理业务,再到数据层获取数据,最后原路返回。

基础设施层(Hilt、性能监控、Crash分析)贯穿始终,为所有层提供支撑。

好了,这一章的内容就到这里。项目架构搭好了,后面每一章都会在这个骨架上填充血肉。

记住:好的架构不是设计出来的,是演进出来的。别追求一步到位,先跑起来,再优化。


公众号:蓝海资料掘金营,微信deep3321