目录
  1. 1. 一、架构层次总览
  2. 2. 二、Repository + Room + Retrofit + Paging
  3. 3. 三、依赖注入:Hilt
  4. 4. 四、Single Activity + Navigation
  5. 5. 五、测试策略
  6. 6. 六、整体依赖关系
  7. 7. 面试常考问题
JetPack全家桶(十)之从0到1设计JetPack架构

本文以设计一个新闻客户端为例,展示如何用 Jetpack 全套组件从 0 到 1 搭建符合 Google 官方推荐的 MVVM 应用架构。

参考:Android Architecture Guide

一、架构层次总览

┌──────────────────────────────────────────────┐
│ UI Layer (Fragment/Activity + ViewModel) │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ DataBinding │ │ Navigation Compose │ │
│ └─────────────┘ └──────────────────────┘ │
├──────────────────────────────────────────────┤
│ Domain / Repository Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Room │ │ Retrofit │ │ WorkManager │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
├──────────────────────────────────────────────┤
│ Data Layer │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Local Source │ │ Remote Source │ │
│ │ (Room/Raw) │ │ (Retrofit/OkHttp) │ │
│ └──────────────┘ └──────────────────────┘ │
└──────────────────────────────────────────────┘

二、Repository + Room + Retrofit + Paging

离线优先的核心:Repository 作为单一入口,优先取本地数据,后台从网络拉取并缓存。

class NewsRepository @Inject constructor(
private val newsDao: NewsDao,
private val api: NewsApiService
) {
// 返回 Flow,Room 自动在数据变更时推送新值
fun getNewsStream(): Flow<PagingData<News>> = Pager(
config = PagingConfig(pageSize = 20),
remoteMediator = NewsRemoteMediator(newsDao, api)
) {
newsDao.pagingSource()
}.flow.cachedIn(CoroutineScope(Dispatchers.IO))
}

三、依赖注入:Hilt

@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides @Singleton
fun provideNewsDao(db: AppDatabase): NewsDao = db.newsDao()

@Provides @Singleton
fun provideApi(): NewsApiService = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(NewsApiService::class.java)
}

Hilt 通过 @AndroidEntryPoint 注入 Activity/Fragment,通过 @HiltViewModel 注入 ViewModel 的 Repository 依赖。

四、Single Activity + Navigation

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

val navController = findNavController(R.id.nav_host_fragment)
binding.bottomNav.setupWithNavController(navController)

// 全局 Toolbar 与 Navigation 联动
setupActionBarWithNavController(navController)
}
}

五、测试策略

  • ViewModel 单元测试:注入 Fake Repository,测试数据转换逻辑。
  • Repository 集成测试:使用 Room 内存数据库 + MockWebServer 测试 RemoteMediator 分页逻辑。
  • UI 测试:使用 Espresso + Navigation 测试导航流程。

六、整体依赖关系

Activity (DI entry)
└── NavHostFragment
├── HomeFragment → HomeViewModel → NewsRepository
│ ├── Room (NewsDao)
│ └── Retrofit (NewsApi)
└── DetailFragment → DetailViewModel (by viewModels)

面试常考问题

Q1:为什么推荐 Single Activity 架构?

减少 Activity 跨进程通信(Binder)开销,Navigation 组件接管 Fragment 跳转栈,避免多 Activity 的场景(如多窗口、生命周期不一致)带来的复杂性。而且在 Jetpack Compose 时代,Single Activity 是唯一正确的选择。

Q2:Repository 是否必须?能否让 ViewModel 直接调用 ApiService?

Repository 是推荐模式,但不强制。它提供了统一的接口,让 ViewModel 不关心数据来源(本地或远程),同时便于替换数据源(如单元测试时注入 Fake Repository)。直接调用 ApiService 会使 ViewModel 职责过重,违反单一职责原则。

Q3:如何避免 Paging 3 的 RemoteMediator 重复插入数据?

在数据库 DAO 中使用 @Insert(onConflict = OnConflictStrategy.REPLACE)@Upsert,以唯一键处理重复。同时在 REFRESH 时调用 clearAll() + insertAll() 覆盖数据。RemoteMediator 内部自动处理并发加载,但需确保数据源设计合理。

打赏
  • 微信
  • 支付宝

评论