H.Step
H.Step

Reputation: 115

How to share HttpClient between Multiplatform Ktor and Coil?

I want to use Coil image library to load images from the api with the same cookie that was set before. Therefore I want to use the same HttpClient both for my Ktor networking calls and for Image Loading with Coil.

How can I share the same HttpClient between Ktor and Coil? I assume, I need to adjust dependencies somehow, but I can't wrap my head around it.

My KtorApiImpl in shared module

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

}

actual HttpClientProvider in 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 in androidApp - how to use an HttpClient instead of creating new?

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

}

Koin dependencies in 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!") }
            }
        }
        )
    }
}

And then Main Activity

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

Upvotes: 4

Views: 1941

Answers (5)

Behnam Maboudi
Behnam Maboudi

Reputation: 684

We can create a composable function for creating ImageRequest and inject our httpclient into the ImageRequest:

@Composable
fun coilImageRequest(): ImageRequest.Builder {
    val httpClient: HttpClient = koinInject()
    return ImageRequest.Builder(LocalPlatformContext.current).fetcherFactory(
        KtorNetworkFetcherFactory(httpClient)
    )
}

And use it like this:

AsyncImage(
      modifier = Modifier,
      model = coilImageRequest()
                .data(imgUrl)
                .crossfade(true)
                .build(),
      contentDescription = null,
      imageLoader = SingletonImageLoader.get(LocalPlatformContext.current))

Upvotes: 1

PenguinDan
PenguinDan

Reputation: 934

If you are using a dependency injection framework, you can attack this by creating a common OkHttp client and sharing that with Ktor and your other implementations

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

Then inject it into your various implementations

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

Upvotes: 0

YektaDev
YektaDev

Reputation: 540

I used to follow the approach suggested by H.Step, and it used to work just fine. However, that was over a year ago and I assume things have just changed.

If you look at the type of OkHttpConfig.preconfigured, you realize that it may be null. For whatever reason, it is null when I try to access it.

A simple solution to ensure that the solution will keep working in this situation is to initialize it if it's null. In fact, this is what OkHttpEngine.createOkHttpClient(...) currently does under the hood. If you take a peek at the source code, you see the following line:

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

So, this is what I did:

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

Upvotes: 0

H.Step
H.Step

Reputation: 115

I accessed the OkHttpClient from ImageLoader with

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

Upvotes: 2

Amr Yousef
Amr Yousef

Reputation: 594

You could use ImageLoader.Builder.callFactory{} to provide your own Call.Factory used for network requests. The downside is you would have to map whatever type your KtorApiImpl returns to okttp3.Response which Coil understands.

Here’s a sample that describes how to implement the Call.Factory interface and provide it to Coil’s 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
                        }
                    }
                }
            }

Upvotes: 1

Related Questions