Skip to content

Commit 5f06121

Browse files
committed
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
1 parent 3ff5d48 commit 5f06121

File tree

3 files changed

+93
-31
lines changed

3 files changed

+93
-31
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.samples.apps.nowinandroid.util
18+
19+
import androidx.tracing.trace
20+
import coil.ImageLoader
21+
import coil.ImageLoaderFactory
22+
import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope
23+
import kotlinx.coroutines.CoroutineScope
24+
import kotlinx.coroutines.Deferred
25+
import kotlinx.coroutines.async
26+
import kotlinx.coroutines.launch
27+
import kotlinx.coroutines.runBlocking
28+
import javax.inject.Inject
29+
30+
/**
31+
* This class asynchronously loads the Coil's image loader on a [ApplicationScope], which uses Default dispatcher.
32+
* Reason for this is to prevent initializing Coil (and thus OkHttp internally) with the first image loading
33+
* to prevent skipping frames and performance issues.
34+
*
35+
* Usage:
36+
* - Init creates an async initialization of the image loader.
37+
* - delegate to [newImageLoader] so that Coil can automatically reach for its loader.
38+
*/
39+
class ImageLoaderAsyncFactory @Inject constructor(
40+
@ApplicationScope
41+
appScope: CoroutineScope,
42+
private val imageLoader: dagger.Lazy<ImageLoader>,
43+
) : ImageLoaderFactory {
44+
private lateinit var asyncNewImageLoader: Deferred<ImageLoader>
45+
46+
init {
47+
appScope.launch {
48+
// Initializing asynchronously, but start immediately
49+
asyncNewImageLoader = async { imageLoader.get() }
50+
}
51+
}
52+
53+
/**
54+
* This runBlocking here is on purpose to prevent any unfinished Coil initialization.
55+
* Most likely this will be already initialized by the time we want to show an image on the screen.
56+
*/
57+
override fun newImageLoader() =
58+
trace("NiaImageLoader.runBlocking") { runBlocking { asyncNewImageLoader.await() } }
59+
}

app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,20 @@
1717
package com.google.samples.apps.nowinandroid
1818

1919
import android.app.Application
20-
import coil.ImageLoader
21-
import coil.ImageLoaderFactory
20+
import coil.Coil
2221
import com.google.samples.apps.nowinandroid.sync.initializers.Sync
22+
import com.google.samples.apps.nowinandroid.util.ImageLoaderAsyncFactory
2323
import com.google.samples.apps.nowinandroid.util.ProfileVerifierLogger
2424
import dagger.hilt.android.HiltAndroidApp
2525
import javax.inject.Inject
26-
import javax.inject.Provider
2726

2827
/**
2928
* [Application] class for NiA
3029
*/
3130
@HiltAndroidApp
32-
class NiaApplication : Application(), ImageLoaderFactory {
31+
class NiaApplication : Application() {
3332
@Inject
34-
lateinit var imageLoader: Provider<ImageLoader>
33+
lateinit var imageLoaderAsyncFactory: ImageLoaderAsyncFactory
3534

3635
@Inject
3736
lateinit var profileVerifierLogger: ProfileVerifierLogger
@@ -41,7 +40,7 @@ class NiaApplication : Application(), ImageLoaderFactory {
4140
// Initialize Sync; the system responsible for keeping data in the app up to date.
4241
Sync.initialize(context = this)
4342
profileVerifierLogger()
43+
// We set immediately Coil's image loader factory to prevent initialization with the first image.
44+
Coil.setImageLoader(imageLoaderAsyncFactory)
4445
}
45-
46-
override fun newImageLoader(): ImageLoader = imageLoader.get()
4746
}

core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.samples.apps.nowinandroid.core.network.di
1818

1919
import android.content.Context
20+
import androidx.tracing.trace
2021
import coil.ImageLoader
2122
import coil.decode.SvgDecoder
2223
import coil.util.DebugLogger
@@ -51,16 +52,18 @@ internal object NetworkModule {
5152

5253
@Provides
5354
@Singleton
54-
fun okHttpCallFactory(): Call.Factory = OkHttpClient.Builder()
55-
.addInterceptor(
56-
HttpLoggingInterceptor()
57-
.apply {
58-
if (BuildConfig.DEBUG) {
59-
setLevel(HttpLoggingInterceptor.Level.BODY)
60-
}
61-
},
62-
)
63-
.build()
55+
fun okHttpCallFactory(): Call.Factory = trace("NiaOkHttpClient") {
56+
OkHttpClient.Builder()
57+
.addInterceptor(
58+
HttpLoggingInterceptor()
59+
.apply {
60+
if (BuildConfig.DEBUG) {
61+
setLevel(HttpLoggingInterceptor.Level.BODY)
62+
}
63+
},
64+
)
65+
.build()
66+
}
6467

6568
/**
6669
* Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this
@@ -72,20 +75,21 @@ internal object NetworkModule {
7275
@Provides
7376
@Singleton
7477
fun imageLoader(
75-
okHttpCallFactory: Call.Factory,
78+
// We specifically request dagger.Lazy here, so that it's not instantiated from Dagger.
79+
okHttpCallFactory: dagger.Lazy<Call.Factory>,
7680
@ApplicationContext application: Context,
77-
): ImageLoader = ImageLoader.Builder(application)
78-
.callFactory(okHttpCallFactory)
79-
.components {
80-
add(SvgDecoder.Factory())
81-
}
82-
// Assume most content images are versioned urls
83-
// but some problematic images are fetching each time
84-
.respectCacheHeaders(false)
85-
.apply {
86-
if (BuildConfig.DEBUG) {
87-
logger(DebugLogger())
81+
): ImageLoader = trace("NiaImageLoader") {
82+
ImageLoader.Builder(application)
83+
.callFactory { okHttpCallFactory.get() }
84+
.components { add(SvgDecoder.Factory()) }
85+
// Assume most content images are versioned urls
86+
// but some problematic images are fetching each time
87+
.respectCacheHeaders(false)
88+
.apply {
89+
if (BuildConfig.DEBUG) {
90+
logger(DebugLogger())
91+
}
8892
}
89-
}
90-
.build()
93+
.build()
94+
}
9195
}

0 commit comments

Comments
 (0)