From 5f0612102d0f1c54cc57039a626efe0126d870c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 29 Jan 2024 23:00:32 +0100 Subject: [PATCH] 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 --- .../util/ImageLoaderAsyncFactory.kt | 59 +++++++++++++++++++ .../apps/nowinandroid/NiaApplication.kt | 13 ++-- .../core/network/di/NetworkModule.kt | 52 ++++++++-------- 3 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/util/ImageLoaderAsyncFactory.kt diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/util/ImageLoaderAsyncFactory.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/util/ImageLoaderAsyncFactory.kt new file mode 100644 index 000000000..540082e50 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/util/ImageLoaderAsyncFactory.kt @@ -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, +) : ImageLoaderFactory { + private lateinit var asyncNewImageLoader: Deferred + + 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() } } +} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt index 9f0bb2ef7..40fffc4d9 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt @@ -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 + 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() } diff --git a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt index 21d93c0e4..a68683c7c 100644 --- a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt @@ -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,16 +52,18 @@ internal object NetworkModule { @Provides @Singleton - fun okHttpCallFactory(): Call.Factory = OkHttpClient.Builder() - .addInterceptor( - HttpLoggingInterceptor() - .apply { - if (BuildConfig.DEBUG) { - setLevel(HttpLoggingInterceptor.Level.BODY) - } - }, - ) - .build() + fun okHttpCallFactory(): Call.Factory = trace("NiaOkHttpClient") { + OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor() + .apply { + if (BuildConfig.DEBUG) { + setLevel(HttpLoggingInterceptor.Level.BODY) + } + }, + ) + .build() + } /** * Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this @@ -72,20 +75,21 @@ 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, @ApplicationContext application: Context, - ): ImageLoader = ImageLoader.Builder(application) - .callFactory(okHttpCallFactory) - .components { - add(SvgDecoder.Factory()) - } - // Assume most content images are versioned urls - // but some problematic images are fetching each time - .respectCacheHeaders(false) - .apply { - if (BuildConfig.DEBUG) { - logger(DebugLogger()) + ): 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) + .apply { + if (BuildConfig.DEBUG) { + logger(DebugLogger()) + } } - } - .build() + .build() + } }