前文分析了data层用database的实现,除了数据库,常用的数据源还有网络,因此这里继续分析network模块。
:core:network模块总览
整个network模块的内容相对较少,各个package的功能如下:
package | 功能 |
---|---|
demo | 用assets中的json文件模拟服务器行为的实现,暂不关注 |
di | 依赖注入network组件 |
model | 数据在网络层的呈现 |
retrofit | 网络层数据仓库 |
NiaNetworkDataSource | 网络层向上暴露的接口,封装具体实现 |
本次分析真实业务逻辑,先跳过单元测试、模拟数据的过程,待完成主要功能拆解分析后,再补齐这些部分。
model包------网络层数据表现形式
仍然离不开Topic
、NewsResource
这两个基本数据类,在model包中提供了网络层的对应实现。
kotlin
@Serializable // 默认使用JSON进行序列化,可见NetworkModule类
data class NetworkTopic(
val id: String,
val name: String = "",
val shortDescription: String = "",
val longDescription: String = "",
val url: String = "",
val imageUrl: String = "",
val followed: Boolean = false,
)
fun NetworkTopic.asExternalModel(): Topic = // 转换为基本数据类型
Topic(
id = id,
name = name,
shortDescription = shortDescription,
longDescription = longDescription,
url = url,
imageUrl = imageUrl,
)
为进行版本管理,提供了NetworkChangeList
数据类,其中封装了数据id
、版本号
、删除标记
。
kotlin
@Serializable
data class NetworkChangeList(
/**
* The id of the model that was changed
*/
val id: String,
/**
* Unique consecutive, monotonically increasing version number in the collection describing
* the relative point of change between models in the collection
*/
val changeListVersion: Int, // model在服务器的递增版本号
/**
* Summarizes the update to the model; whether it was deleted or updated.
* Updates include creations.
*/
val isDelete: Boolean,
)
di包------网络模块单例
di包只含有NetworkModule
一个类,它提供了网络模块的OkHttp单例实现。
kotlin
@Module
@InstallIn(SingletonComponent::class) // 很好理解,作用域应用全生命周期的单例对象
internal object NetworkModule {
@Provides // 创建单例,供全局使用
@Singleton
fun providesNetworkJson(): Json = Json { // 提供Json单例,在使用处调用@Inject进行注入
ignoreUnknownKeys = true
}
@Provides
@Singleton
fun okHttpCallFactory(): Call.Factory = trace("NiaOkHttpClient") { // Systrace埋点
OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor() // 打印日志拦截器
.apply {
if (BuildConfig.DEBUG) {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
},
)
.build()
}
@Provides
@Singleton
fun imageLoader( // 全局使用的单例图片加载器,同样用trace进行埋点
// We specifically request dagger.Lazy here, so that it's not instantiated from Dagger.
okHttpCallFactory: dagger.Lazy<Call.Factory>,
@ApplicationContext application: Context,
): ImageLoader = trace("NiaImageLoader") {
ImageLoader.Builder(application)
.callFactory { okHttpCallFactory.get() }
.components { add(SvgDecoder.Factory()) }
// Assume most content images are versioned urls
// but some problematic images are fetching each time
.respectCacheHeaders(false)
.apply {
if (BuildConfig.DEBUG) {
logger(DebugLogger())
}
}
.build()
}
}
知识点1:本项目中使用的单例模式
到现在,已经开始对Hilt
的单例注解慢慢熟悉起来了,@Module
表示这是一个Hilt模块
,其内部提供了全局 使用的各种单例、@InstallIn(SingletonComonent::class)
表示模块的作用域为应用级单例 ,与应用生命周期一致。
在模块内部的方法上@Provides
表示对外提供的注入对象,@Singleton
则表示对象是全局唯一的单例,不会在每次调用时创建一个全新的。
知识点2:Systrace埋点
在创建OkHttpClient
和ImageLoader
时,都使用trace("xxxx"){}
的代码块,包装了网络请求、图片加载的过程。其作用在于监控 关键(耗时) 操作,这种埋点收集的数据会写入system trace buffer
中,可以使用Systrace工具
进行分析,定位耗时操作。
知识点3:coil图片加载器
这里没有使用耳熟能详的Glide进行图片加载,而是使用了Coil
。Coil是JetBrains开发的用于图片加载的开源框架,其与老牌的Glide对比如下:
比较项 | Coil | Glide |
---|---|---|
开发语言 | 纯 Kotlin(协程优先) | Java(兼容 Kotlin) |
包体积 | ≈200 KB(无额外依赖) | ≈500 KB(含解码库) |
API 风格 | 函数式 DSL,Jetpack Compose 原生支持 | 链式调用,需要配置 RequestOptions |
默认内存管理 | 自动复用 Bitmap,协程轻量化 | Bitmap 池 + 四级缓存 |
GIF 支持 | 需依赖 coil-gif | 原生支持 |
学习曲线 | 简单(Kotlin 友好) | 中等(复杂定制需深入 API) |
retrofit包------构建单例服务器数据仓库
retrofit包里面只有RetrofitNiaNetwork
一个类,它提供初始化Retrofit单例对象、访问服务器接口的功能。
kotlin
private interface RetrofitNiaNetworkApi { // Retrofit自动关联网络请求调用
@GET(value = "topics")
suspend fun getTopics(
@Query("id") ids: List<String>?,
): NetworkResponse<List<NetworkTopic>>
@GET(value = "newsresources")
suspend fun getNewsResources(
@Query("id") ids: List<String>?,
): NetworkResponse<List<NetworkNewsResource>>
@GET(value = "changelists/topics")
suspend fun getTopicChangeList(
@Query("after") after: Int?,
): List<NetworkChangeList>
@GET(value = "changelists/newsresources")
suspend fun getNewsResourcesChangeList(
@Query("after") after: Int?,
): List<NetworkChangeList>
}
// 泛型,封装网络请求结果
@Serializable
private data class NetworkResponse<T>(
val data: T,
)
以下是Retrofit数据源的构建过程,与database相同,它也是一个单例。
kotlin
@Singleton
internal class RetrofitNiaNetwork @Inject constructor(
networkJson: Json,
okhttpCallFactory: dagger.Lazy<Call.Factory>,
) : NiaNetworkDataSource {
private val networkApi = trace("RetrofitNiaNetwork") { // Systrace跟踪
Retrofit.Builder()
.baseUrl(NIA_BASE_URL)
// We use callFactory lambda here with dagger.Lazy<Call.Factory>
// to prevent initializing OkHttp on the main thread.
.callFactory { okhttpCallFactory.get().newCall(it) } // 防止在主线程初始化
.addConverterFactory(
networkJson.asConverterFactory("application/json".toMediaType()),
)
.build()
.create(RetrofitNiaNetworkApi::class.java)
}
override suspend fun getTopics(ids: List<String>?): List<NetworkTopic> =
networkApi.getTopics(ids = ids).data
override suspend fun getNewsResources(ids: List<String>?): List<NetworkNewsResource> =
networkApi.getNewsResources(ids = ids).data
override suspend fun getTopicChangeList(after: Int?): List<NetworkChangeList> =
networkApi.getTopicChangeList(after = after)
override suspend fun getNewsResourceChangeList(after: Int?): List<NetworkChangeList> =
networkApi.getNewsResourcesChangeList(after = after)
}