如何在 Multiplatform Ktor 和 Coil 之间共享 HttpClient?

Posted

技术标签:

【中文标题】如何在 Multiplatform Ktor 和 Coil 之间共享 HttpClient?【英文标题】:How to share HttpClient between Multiplatform Ktor and Coil? 【发布时间】:2021-10-06 02:07:31 【问题描述】:

我想使用 Coil 图像库从 api 加载图像,使用之前设置的相同 cookie。因此,我想为我的 Ktor 网络调用和使用线圈的图像加载使用相同的 HttpClient。

如何在 Ktor 和 Coil 之间共享同一个 HttpClient?我想,我需要以某种方式调整依赖关系,但我无法理解它。

共享模块中的我的 KtorApiImpl

class KtorApiImpl(log: Kermit) : KtorApi 
val baseUrl = BuildKonfig.baseUrl

// If this is a constructor property, then it gets captured
// inside HttpClient config and freezes this whole class.
@Suppress("CanBePrimaryConstructorProperty")
private val log = log

override val client = HttpClientProvider().getHttpClient().config 
    install(JsonFeature) 
        serializer = KotlinxSerializer()
    
    install(Logging) 
        logger = object : Logger 
            override fun log(message: String) 
                log.v("Network")  message 
            
        

        level = LogLevel.INFO
    


init 
    ensureNeverFrozen()


override fun HttpRequestBuilder.apiUrl(path: String) 
    url 
        takeFrom(baseUrl)
        encodedPath = path
    


override fun HttpRequestBuilder.json() 
    contentType(ContentType.Application.Json)

androidMain 中的实际 HttpClientProvider

var cookieJar: CookieJar = object : CookieJar 
    private val cookieStore: HashMap<String, List<Cookie>> = HashMap()

    override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) 
        cookieStore[url.host] = cookies
    

    override fun loadForRequest(url: HttpUrl): List<Cookie> 
        val cookies = cookieStore[url.host]
        return cookies ?: ArrayList()
    



actual class HttpClientProvider actual constructor() 
    actual fun getHttpClient(): HttpClient 
        return HttpClient(OkHttp) 
            engine 
                preconfigured = getOkHttpClient()
            
        
    


private fun getOkHttpClient(): OkHttpClient 
    return OkHttpClient.Builder()
        .cookieJar(cookieJar)
        .build()

androidApp 中的 ImageLoaderFactory - 如何使用 HttpClient 而不是创建新的?

class CoilImageLoaderFactory(private val context: Context) : ImageLoaderFactory 
    override fun newImageLoader(): ImageLoader 
        return ImageLoader.Builder(context)
            .availableMemoryPercentage(0.25) // Use 25% of the application's available memory.
            .crossfade(true) // Show a short crossfade when loading images from network or disk.
            .componentRegistry 
                add(ByteArrayFetcher())
            
            .okHttpClient 
                // Create a disk cache with "unlimited" size. Don't do this in production.
                // To create the an optimized Coil disk cache, use CoilUtils.createDefaultCache(context).
                val cacheDirectory = File(context.filesDir, "image_cache").apply  mkdirs() 
                val cache = Cache(cacheDirectory, Long.MAX_VALUE)

                // Lazily create the OkHttpClient that is used for network operations.
                OkHttpClient.Builder()
                    .cache(cache)
                    .build()
            
            .build()
    


androidApp 中的 Koin 依赖项

@Suppress("unused")
class MainApp : Application() 

    override fun onCreate() 
        super.onCreate()
        initKoin(
        module 
            single<Context>  this@MainApp 
            single<AppInfo>  AndroidAppInfo 
            single  CoilImageLoaderFactory(get<Context>())
            single<SharedPreferences> 
                get<Context>().getSharedPreferences("MAIN_SETTINGS", Context.MODE_PRIVATE)
            
            single 
                 Log.i("Startup", "Hello from Android/Kotlin!") 
            
        
        )
    

然后是主活动

class MainActivity : AppCompatActivity()  
    val loaderFactory: CoilImageLoaderFactory by inject()

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContent 
            CompositionLocalProvider(LocalImageLoader provides loaderFactory.newImageLoader()) 
                MainTheme 
                    ProvideWindowInsets 
                        Surface 
                            MainScreen()
                        
                    
                
            
        
    

【问题讨论】:

【参考方案1】:

您可以使用ImageLoader.Builder.callFactory 提供您自己的Call.Factory 用于网络请求。缺点是您必须将 KtorApiImpl 返回的任何类型映射到 Coil 可以理解的 okttp3.Response

这是一个示例,描述了如何实现Call.Factory 接口并将其提供给 Coil 的ImageLoader

ImageLoader.Builder(context)
            .callFactory 
                Call.Factory 
                    object: Call 
                        private var job: Job? = null
                        override fun clone(): Call 
                            TODO(“Not yet implemented”)
                        

                        override fun request(): Request 
                            return it
                        

                        override fun execute(): Response 
                            return runBlocking 
                                // Call KTOR client here
                            
                        

                        override fun enqueue(responseCallback: Callback) 
                            // Use a proper coroutines scope
                            job = GlobalScope.launch 
                                // Call KTOR client here
                            
                        

                        override fun cancel() 
                            job?.cancel()
                        

                        override fun isExecuted(): Boolean 
                            return job?.isCompleted ?: false
                        

                        override fun isCanceled(): Boolean 
                            return job?.isCancelled ?: false
                        

                        override fun timeout(): Timeout 
                            // Your Timeout here
                        
                    
                
            

【讨论】:

【参考方案2】:

我通过 ImageLoader 访问了 OkHttpClient

class CoilImageLoaderFactory(private val context: Context) : ImageLoaderFactory, KoinComponent 
val ktorApiImpl: KtorApi by inject()

override fun newImageLoader(): ImageLoader 
    return ImageLoader.Builder(context)
        .componentRegistry 
            add(ByteArrayFetcher())
        
        .okHttpClient 
            val config = ktorApiImpl.client.engine.config as OkHttpConfig
            config.preconfigured as OkHttpClient
            
        


        .build()

【讨论】:

以上是关于如何在 Multiplatform Ktor 和 Coil 之间共享 HttpClient?的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin Multiplatform - 使用 Ktor 处理响应 http 代码和异常

kotlin.native.concurrent.InvalidMutabilityException:在 Kotlin Multiplatform (iOS) 中使用 ktor 时冻结 <ob

在 Kotlin MultiPlatform 项目中未解决 iOS 依赖项

Kotlin Multiplatform Cocoapods 集成问题

Kotlin Ktor 客户端 mltiplatform gradle 配置

Kotlin-multiplatform:如何执行 iOS 单元测试