Reputation: 115
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
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
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
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
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
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