NowInAndroid学习系列
Now in Android !AndroidApp开发的最佳实践,让我看看是怎么个事?
Now in Android学习Compose是怎么切换主题
在我们Android项目中,网络模块算是项目中比较重要的模块之一了。学习一下如何更好的封装网络模块,也是很有必要的。
1、Now In Android的网络模块的划分
在NowInAndroid中,网络请求的部分,是通过ViewModel注入数据仓库,在数据仓库里注入Retrofit来实现网络请求的。 数据仓库是一个模块,datastore 一个模块,database一个模块,network一个模块,数据仓库分别依赖着三个模块
依赖关系 data模块的gradle dependencies 代码
dependencies {
api(projects.core.common)
api(projects.core.database)
api(projects.core.datastore)
api(projects.core.network)
}
✅ api 作用:将依赖项暴露给当前模块的消费者(即使用该模块的其他模块)。 依赖传递性:如果模块 A 使用 api 引入了模块 B,那么使用模块 A 的模块 C 也能直接访问模块 B。 适用场景: 当你的模块是一个库模块,并且你希望它的依赖对使用者是透明可见的。 比如你在封装一个 SDK,SDK 内部用到了某个库,而这个库也需要被接入 SDK 的 App 所使用。 ⚠️ 注意:滥用 api 会增加编译时间并可能导致依赖冲突。
✅ implementation 作用:只在当前模块内部使用该依赖,不会暴露给使用该模块的其他模块。 依赖传递性:依赖仅限于当前模块,不会传递给外部。 适用场景: 当前模块使用的依赖不需要被外部访问时。 推荐优先使用 implementation 来减少不必要的依赖泄露和潜在冲突。
2、NetWork具体封装
2.1 数据源接口
根据业务定义数据源接口。这个是根据业务逻辑来的
interface NiaNetworkDataSource {
suspend fun getTopics(ids: List<String>? = null): List<NetworkTopic>
suspend fun getNewsResources(ids: List<String>? = null): List<NetworkNewsResource>
suspend fun getTopicChangeList(after: Int? = null): List<NetworkChangeList>
suspend fun getNewsResourceChangeList(after: Int? = null): List<NetworkChangeList>
}
2.2 数据源的实现
2.2.1 API接口
private interface RetrofitNiaNetworkApi {
@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>
}
private const val NIA_BASE_URL = BuildConfig.BACKEND_URL
2.2.2 返回体的封装
这里还是非常简单的
/**
* Wrapper for data provided from the [NIA_BASE_URL]
*/
@Serializable
private data class NetworkResponse<T>(
val data: T,
)
2.2.3 数据源的实现
@Singleton
internal class RetrofitNiaNetwork @Inject constructor(
networkJson: Json,
okhttpCallFactory: dagger.Lazy<Call.Factory>,
) : NiaNetworkDataSource {
private val networkApi = trace("RetrofitNiaNetwork") {
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)
}
2.3 数据源实现小节
Retrofit的API和 Response的封装都是private的,都在一个文件里,都在这个数据源实现的文件里RetrofitNiaNetwork,API一层,数据源一层,数据仓库一层。最后到ViewModel,这封装的层数,比我的命还要长,可能我写的都是小项目,感觉没必要封装那么多层。而且都是用的挂起函数,异步起来也是非常的方便。
3、网络请求的逻辑,和异常处理
在NowInAndroid中,界面显示的数据其实是存在数据库里,但是他也会存到服务器,他有一个Worker,启动的时候,会开启同步任务,将本地的数据和远程的服务器数据进行同步。 是在一个协程里做的任务,而且用了TryCatch,捕获了异常,任务标记失败
@Suppress("DEPRECATION")
public final override fun startWork(): ListenableFuture<Result> {
val coroutineScope = CoroutineScope(coroutineContext + job)
coroutineScope.launch {
try {
val result = doWork()
future.set(result)
} catch (t: Throwable) {
future.setException(t)
}
}
return future
}
dowork
override suspend fun doWork(): Result = withContext(ioDispatcher) {
traceAsync("Sync", 0) {
analyticsHelper.logSyncStarted()
syncSubscriber.subscribe()
// First sync the repositories in parallel
val syncedSuccessfully = awaitAll(
async { topicRepository.sync() },
async { newsRepository.sync() },
).all { it }
analyticsHelper.logSyncFinished(syncedSuccessfully)
if (syncedSuccessfully) {
searchContentsRepository.populateFtsData()
Result.success()
} else {
Result.retry()
}
}
}
同步操作
override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
synchronizer.changeListSync(
versionReader = ChangeListVersions::topicVersion,
changeListFetcher = { currentVersion ->
network.getTopicChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(topicVersion = latestVersion)
},
modelDeleter = topicDao::deleteTopics,
modelUpdater = { changedIds ->
val networkTopics = network.getTopics(ids = changedIds)
topicDao.upsertTopics(
entities = networkTopics.map(NetworkTopic::asEntity),
)
},
)
suspend fun Synchronizer.changeListSync(
versionReader: (ChangeListVersions) -> Int,
changeListFetcher: suspend (Int) -> List<NetworkChangeList>,
versionUpdater: ChangeListVersions.(Int) -> ChangeListVersions,
modelDeleter: suspend (List<String>) -> Unit,
modelUpdater: suspend (List<String>) -> Unit,
) = suspendRunCatching {
// Fetch the change list since last sync (akin to a git fetch)
val currentVersion = versionReader(getChangeListVersions())
val changeList = changeListFetcher(currentVersion)
if (changeList.isEmpty()) return@suspendRunCatching true
val (deleted, updated) = changeList.partition(NetworkChangeList::isDelete)
// Delete models that have been deleted server-side
modelDeleter(deleted.map(NetworkChangeList::id))
// Using the change list, pull down and save the changes (akin to a git pull)
modelUpdater(updated.map(NetworkChangeList::id))
// Update the last synced version (akin to updating local git HEAD)
val latestVersion = changeList.last().changeListVersion
updateChangeListVersions {
versionUpdater(latestVersion)
}
}.isSuccess