Как поделиться HttpClient между мультиплатформенным Ktor и Coil?

Я хочу использовать библиотеку изображений Coil для загрузки изображений из api с тем же файлом cookie, который был установлен ранее. Поэтому я хочу использовать один и тот же HttpClient как для сетевых вызовов Ktor, так и для загрузки изображений с помощью Coil.

Как я могу использовать один и тот же HttpClient между Ktor и Coil? Я предполагаю, что мне нужно как-то настроить зависимости, но я не могу об этом думать.

Мой 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)
}

}

фактический HttpClientProvider в androidMain

      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()
}

ImageLoaderFactory в androidApp - как использовать 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

      @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()
                        }
                    }
                }
            }
        }
    }
}

4 ответа

Решение

Я получил доступ к OkHttpClient из ImageLoader с помощью

      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()
}

Вы могли бы использовать ImageLoader.Builder.callFactory{}чтобы предоставить свой собственный, используемый для сетевых запросов. Обратной стороной является то, что вам придется отображать любой тип вашего KtorApiImpl возвращается к okttp3.Response что Coil понимает.

Вот пример, который описывает, как реализовать 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
                        }
                    }
                }
            }

Раньше я следовал подходу, предложенному user7692155 , и он работал нормально. Однако это было больше года назад, и я предполагаю, что все только что изменилось.

Если вы посмотрите на типOkHttpConfig.preconfigured, ты понимаешь, что это может быть. По какой-то причине это происходит , когда я пытаюсь получить к нему доступ.

Простое решение, гарантирующее, что решение будет продолжать работать в этой ситуации, — это инициализировать его, если оноnull. На самом деле, это то, чтоOkHttpEngine.createOkHttpClient(...)на данный момент делает под капотом. Если вы взглянете на исходный код , вы увидите следующую строку:

      private fun createOkHttpClient(timeoutExtension: HttpTimeout.HttpTimeoutCapabilityConfiguration?): OkHttpClient {
    val builder = (config.preconfigured ?: okHttpClientPrototype).newBuilder()
    // ...
}

Итак, вот что я сделал:

      class App : Application(), ImageLoaderFactory {
    override fun newImageLoader(): ImageLoader = ImageLoader
      .Builder(this)
      .callFactory {
          val config = myApi.client.engine.config as OkHttpConfig
          if (config.preconfigured == null) {
              config.preconfigured = OkHttpClient.Builder().build()
          }
          config.preconfigured as OkHttpClient
      }
      .build()
}

Если вы используете среду внедрения зависимостей, вы можете атаковать ее, создав общий клиент OkHttp и поделившись им с Ktor и другими вашими реализациями.

      // Only OkHttp client to be shared
@ApplicationScope
@Provides
fun okHttpClient(
    interceptors: Set<Interceptor>,
): OkHttpClient {
    return OkHttpClient.Builder()
        .apply { interceptors.forEach(this::addInterceptor) }
        .build()
}

Затем внедрите его в свои различные реализации.

      @ApplicationScope
fun httpClient(
    okHttpClient: OkHttpClient,
): HttpClient {
    return HttpClient(OkHttp) {
        engine {
            preconfigured = okHttpClient
        }

        install(ContentNegotiation) {
            json()
        }
    }
}
      @ApplicationScope
@Provides
fun apolloClient(
    okHttpClient: OkHttpClient,
): ApolloClient {
    return ApolloClient.Builder()
        .serverUrl("...")
        .httpEngine(DefaultHttpEngine(okHttpClient))
        .build()
}
Другие вопросы по тегам