一、项目架构设计——从零搭建新闻客户端
说实话,每次开始一个新项目,我最头疼的不是写代码,而是「架构怎么搭」。搭好了后面顺风顺水,搭不好……嗯,后面全是坑。
这次我们要做一个新闻客户端。功能不算复杂:展示新闻列表、查看详情、收藏、分类浏览。但麻雀虽小五脏俱全,正好拿它来演示 Jetpack 全家桶怎么配合。
1.1 整体架构分层
我个人习惯用「分层架构」来组织代码。说白了就是各干各的,互不干扰。我把它分成三层:
- UI 层:Activity、Fragment、Compose 页面,只管展示和交互
- 数据层:Repository、DataSource,管数据从哪来、存哪去
- 依赖注入层:Hilt 模块,管对象怎么创建、怎么给
你想想看,如果这三层混在一起,改个网络请求库,UI 层也得跟着改,那多痛苦。我在项目中遇到过这种「面条代码」,改一个 bug 能引出三个新 bug……
核心原则:上层依赖下层,下层不依赖上层。UI 层只知道 Repository,不知道 Room 还是 Retrofit。
1.2 模块划分
项目我拆成了这几个模块:
| 模块名 | 职责 | 依赖 |
|---|---|---|
| app | 主入口,Application 类 | 所有模块 |
| core-data | 数据层,Repository 实现 | core-network, core-database |
| core-network | 网络请求,Retrofit 封装 | 无 |
| core-database | 本地缓存,Room 封装 | 无 |
| feature-news | 新闻列表、详情 UI | core-data |
| feature-bookmark | 收藏功能 UI | core-data |
为什么要分这么细?我曾经在一个大项目里把所有代码塞进一个 module,结果编译一次 5 分钟……后来拆成模块,编译时间直接砍半。而且团队协作时,每个人负责一个 feature 模块,冲突少很多。
二、依赖注入——Hilt 让代码更清爽
依赖注入这东西,说白了就是「你要什么,我给你什么,你不用自己 new」。我刚开始用 Dagger 时被它的配置搞到头大,后来 Hilt 出来,嗯,舒服多了。
2.1 安装 Hilt
在项目级 build.gradle 加插件:
// 项目级 build.gradle
plugins {
id 'com.google.dagger.hilt.android' version '2.48' apply false
}
// app/build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.dagger.hilt.android'
id 'kotlin-kapt'
}
dependencies {
implementation 'com.google.dagger:hilt-android:2.48'
kapt 'com.google.dagger:hilt-compiler:2.48'
}
然后给 Application 类加个注解:
@HiltAndroidApp
class NewsApplication : Application()
就这么简单。Hilt 会自动生成 Dagger 组件,你不用写 Component 接口了。
2.2 提供依赖——Module 怎么写
我们需要告诉 Hilt:「嘿,Retrofit 怎么创建?Room 数据库怎么创建?」
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideNewsApi(retrofit: Retrofit): NewsApi {
return retrofit.create(NewsApi::class.java)
}
}
小提示:用 @Singleton 注解确保整个应用只有一个 Retrofit 实例。我见过有人每次请求都 new 一个 Retrofit,那性能……嗯,你懂的。
2.3 注入到 ViewModel
ViewModel 里直接用构造函数注入:
@HiltViewModel
class NewsViewModel @Inject constructor(
private val newsRepository: NewsRepository
) : ViewModel() {
private val _newsList = MutableStateFlow<List<News>>(emptyList())
val newsList: StateFlow<List<News>> = _newsList.asStateFlow()
fun loadNews() {
viewModelScope.launch {
val result = newsRepository.getNews()
_newsList.value = result
}
}
}
你看,ViewModel 里没有一行 new 的代码。所有依赖都是「要来的」。这样测试时也方便,直接 mock Repository 传进去就行。
三、数据层实现——Repository 模式
数据层我习惯用 Repository 模式。它就像一个「数据管家」,UI 层只管问它要数据,不用管数据是从网络来还是从缓存来。
3.1 定义数据源接口
// 网络数据源
interface NewsRemoteDataSource {
suspend fun fetchNews(): List<News>
}
// 本地数据源
interface NewsLocalDataSource {
suspend fun getCachedNews(): List<News>
suspend fun saveNews(news: List<News>)
}
3.2 Repository 实现
class NewsRepositoryImpl @Inject constructor(
private val remoteDataSource: NewsRemoteDataSource,
private val localDataSource: NewsLocalDataSource
) : NewsRepository {
override suspend fun getNews(): List<News> {
// 先尝试从网络获取
return try {
val remoteNews = remoteDataSource.fetchNews()
localDataSource.saveNews(remoteNews) // 缓存到本地
remoteNews
} catch (e: Exception) {
// 网络失败,从本地缓存读取
localDataSource.getCachedNews()
}
}
}
注意:这里有个坑。如果网络请求失败,直接返回缓存数据,但用户可能看到的是旧数据。我曾经在项目中加了一个「缓存时间」判断:如果缓存超过 30 分钟,即使网络失败也要提示用户「数据可能不是最新的」。
四、UI 层实现——Compose + StateFlow
UI 层我用 Jetpack Compose,配合 StateFlow 做状态管理。为什么选 Compose?因为声明式 UI 和 StateFlow 简直是天生一对。
4.1 新闻列表页面
@Composable
fun NewsListScreen(
viewModel: NewsViewModel = hiltViewModel()
) {
val newsList by viewModel.newsList.collectAsState()
LazyColumn {
items(newsList) { news ->
NewsItem(news = news) {
// 点击跳转详情
}
}
}
}
@Composable
fun NewsItem(news: News, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { onClick() }
) {
Row(modifier = Modifier.padding(16.dp)) {
AsyncImage(
model = news.imageUrl,
contentDescription = null,
modifier = Modifier.size(80.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(text = news.title, fontWeight = FontWeight.Bold)
Text(text = news.summary, maxLines = 2)
}
}
}
}
4.2 状态管理——别忘了一件事
嗯,这里要注意:StateFlow 在 Compose 里用 collectAsState() 收集,但如果你在 Fragment 里用,记得用 lifecycleScope 配合 repeatOnLifecycle,防止泄漏。
// Fragment 中使用
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.newsList.collect { news ->
// 更新 UI
}
}
}
五、实战:新闻客户端完整流程
好了,我们把所有东西串起来。用户打开 App,流程是这样的:
- App 启动,Hilt 初始化所有依赖
- NewsListFragment 创建,NewsViewModel 被注入
- ViewModel 调用 Repository 获取新闻列表
- Repository 先尝试网络请求,失败则读缓存
- 数据通过 StateFlow 发射到 UI 层
- Compose 页面自动重组,展示新闻列表
你想想看,整个过程没有一行代码是「硬耦合」的。换网络库?改 NetworkModule 就行。换本地存储?改 LocalDataSource 实现就行。UI 层完全不受影响。
总结一下:Jetpack 组件不是孤立的,Hilt + Room + Retrofit + Compose + StateFlow 组合起来,才能发挥最大威力。我做了这么多年 Android,这套组合是目前我觉得最「舒服」的架构。
公众号:蓝海资料掘金营,微信deep3321