Now In Android学习怎么封装网络模块

2,961 阅读4分钟

NowInAndroid学习系列

Now in Android !AndroidApp开发的最佳实践,让我看看是怎么个事?

Now in Android学习Compose是怎么切换主题

Now in Androd 大项目是怎么构建程序的

Hilt 在Now In Android的应用

在我们Android项目中,网络模块算是项目中比较重要的模块之一了。学习一下如何更好的封装网络模块,也是很有必要的。

1、Now In Android的网络模块的划分

在NowInAndroid中,网络请求的部分,是通过ViewModel注入数据仓库,在数据仓库里注入Retrofit来实现网络请求的。 数据仓库是一个模块,datastore 一个模块,database一个模块,network一个模块,数据仓库分别依赖着三个模块

image.png

依赖关系 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