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
import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.Coil
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 dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import javax.inject.Provider
/**
* [Application] class for NiA
*/
@HiltAndroidApp
class NiaApplication : Application(), ImageLoaderFactory {
class NiaApplication : Application() {
@Inject
lateinit var imageLoader: Provider<ImageLoader>
lateinit var imageLoaderAsyncFactory: ImageLoaderAsyncFactory
@Inject
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.
Sync.initialize(context = this)
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
import android.content.Context
import androidx.tracing.trace
import coil.ImageLoader
import coil.decode.SvgDecoder
import coil.util.DebugLogger
@ -51,7 +52,8 @@ internal object NetworkModule {
@Provides
@Singleton
fun okHttpCallFactory(): Call.Factory = OkHttpClient.Builder()
fun okHttpCallFactory(): Call.Factory = trace("NiaOkHttpClient") {
OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor()
.apply {
@ -61,6 +63,7 @@ internal object NetworkModule {
},
)
.build()
}
/**
* Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this
@ -72,13 +75,13 @@ internal object NetworkModule {
@Provides
@Singleton
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,
): ImageLoader = ImageLoader.Builder(application)
.callFactory(okHttpCallFactory)
.components {
add(SvgDecoder.Factory())
}
): ImageLoader = trace("NiaImageLoader") {
ImageLoader.Builder(application)
.callFactory { okHttpCallFactory.get() }
.components { add(SvgDecoder.Factory()) }
// Assume most content images are versioned urls
// but some problematic images are fetching each time
.respectCacheHeaders(false)
@ -89,3 +92,4 @@ internal object NetworkModule {
}
.build()
}
}

Loading…
Cancel
Save