Improve lazy loading for Coil + OkHttp

This way, we can load Coil's backend on a background thread and not block the MainThread with it.
Previously, the Coil image loader was initialized with the first composed image, which caused ~10ms duration and most likely skipped frames.

Change-Id: Iaa583b6adc1df7d7a51dbae1473e539f2c0b0b62
pull/1190/head
Tomáš Mlynarič 11 months ago
parent 3ff5d48f37
commit 5f0612102d

@ -0,0 +1,59 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.util
import androidx.tracing.trace
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
/**
* This class asynchronously loads the Coil's image loader on a [ApplicationScope], which uses Default dispatcher.
* Reason for this is to prevent initializing Coil (and thus OkHttp internally) with the first image loading
* to prevent skipping frames and performance issues.
*
* Usage:
* - Init creates an async initialization of the image loader.
* - delegate to [newImageLoader] so that Coil can automatically reach for its loader.
*/
class ImageLoaderAsyncFactory @Inject constructor(
@ApplicationScope
appScope: CoroutineScope,
private val imageLoader: dagger.Lazy<ImageLoader>,
) : ImageLoaderFactory {
private lateinit var asyncNewImageLoader: Deferred<ImageLoader>
init {
appScope.launch {
// Initializing asynchronously, but start immediately
asyncNewImageLoader = async { imageLoader.get() }
}
}
/**
* This runBlocking here is on purpose to prevent any unfinished Coil initialization.
* Most likely this will be already initialized by the time we want to show an image on the screen.
*/
override fun newImageLoader() =
trace("NiaImageLoader.runBlocking") { runBlocking { asyncNewImageLoader.await() } }
}

@ -17,21 +17,20 @@
package com.google.samples.apps.nowinandroid package com.google.samples.apps.nowinandroid
import android.app.Application import android.app.Application
import coil.ImageLoader import coil.Coil
import coil.ImageLoaderFactory
import com.google.samples.apps.nowinandroid.sync.initializers.Sync import com.google.samples.apps.nowinandroid.sync.initializers.Sync
import com.google.samples.apps.nowinandroid.util.ImageLoaderAsyncFactory
import com.google.samples.apps.nowinandroid.util.ProfileVerifierLogger import com.google.samples.apps.nowinandroid.util.ProfileVerifierLogger
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
/** /**
* [Application] class for NiA * [Application] class for NiA
*/ */
@HiltAndroidApp @HiltAndroidApp
class NiaApplication : Application(), ImageLoaderFactory { class NiaApplication : Application() {
@Inject @Inject
lateinit var imageLoader: Provider<ImageLoader> lateinit var imageLoaderAsyncFactory: ImageLoaderAsyncFactory
@Inject @Inject
lateinit var profileVerifierLogger: ProfileVerifierLogger lateinit var profileVerifierLogger: ProfileVerifierLogger
@ -41,7 +40,7 @@ class NiaApplication : Application(), ImageLoaderFactory {
// Initialize Sync; the system responsible for keeping data in the app up to date. // Initialize Sync; the system responsible for keeping data in the app up to date.
Sync.initialize(context = this) Sync.initialize(context = this)
profileVerifierLogger() profileVerifierLogger()
// We set immediately Coil's image loader factory to prevent initialization with the first image.
Coil.setImageLoader(imageLoaderAsyncFactory)
} }
override fun newImageLoader(): ImageLoader = imageLoader.get()
} }

@ -17,6 +17,7 @@
package com.google.samples.apps.nowinandroid.core.network.di package com.google.samples.apps.nowinandroid.core.network.di
import android.content.Context import android.content.Context
import androidx.tracing.trace
import coil.ImageLoader import coil.ImageLoader
import coil.decode.SvgDecoder import coil.decode.SvgDecoder
import coil.util.DebugLogger import coil.util.DebugLogger
@ -51,7 +52,8 @@ internal object NetworkModule {
@Provides @Provides
@Singleton @Singleton
fun okHttpCallFactory(): Call.Factory = OkHttpClient.Builder() fun okHttpCallFactory(): Call.Factory = trace("NiaOkHttpClient") {
OkHttpClient.Builder()
.addInterceptor( .addInterceptor(
HttpLoggingInterceptor() HttpLoggingInterceptor()
.apply { .apply {
@ -61,6 +63,7 @@ internal object NetworkModule {
}, },
) )
.build() .build()
}
/** /**
* Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this * Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this
@ -72,13 +75,13 @@ internal object NetworkModule {
@Provides @Provides
@Singleton @Singleton
fun imageLoader( fun imageLoader(
okHttpCallFactory: Call.Factory, // We specifically request dagger.Lazy here, so that it's not instantiated from Dagger.
okHttpCallFactory: dagger.Lazy<Call.Factory>,
@ApplicationContext application: Context, @ApplicationContext application: Context,
): ImageLoader = ImageLoader.Builder(application) ): ImageLoader = trace("NiaImageLoader") {
.callFactory(okHttpCallFactory) ImageLoader.Builder(application)
.components { .callFactory { okHttpCallFactory.get() }
add(SvgDecoder.Factory()) .components { add(SvgDecoder.Factory()) }
}
// Assume most content images are versioned urls // Assume most content images are versioned urls
// but some problematic images are fetching each time // but some problematic images are fetching each time
.respectCacheHeaders(false) .respectCacheHeaders(false)
@ -88,4 +91,5 @@ internal object NetworkModule {
} }
} }
.build() .build()
}
} }

Loading…
Cancel
Save