Skip to content

Use applicationContext for Singleton Image Loader#3246

Merged
colinrtwhite merged 4 commits into
coil-kt:mainfrom
nishatoma:use-app-context-for-loader
Jan 15, 2026
Merged

Use applicationContext for Singleton Image Loader#3246
colinrtwhite merged 4 commits into
coil-kt:mainfrom
nishatoma:use-app-context-for-loader

Conversation

@nishatoma
Copy link
Copy Markdown
Contributor

@nishatoma nishatoma commented Nov 24, 2025

This PR attempts to fix #3213 by using applicationContext when fetching ImageLoader. This only affects Android platform. Non-Android platforms remains using PlatformContext as is.

Changelog

  • Added applicationContext() extension function.
  • Used said extension function within SingleImageLoader.kt to use applicationContext from the PlatformContext if possible.
  • Added unit test. Verified test is failing after reverting my change.

Before and After breakpoint checking passed context via activity

Before After
before-app-context after-app-context

Before and After LeakCanary dump

This is my first time using LeakCanary, so I would need help to verify if this is working as intended:

Before

 GC Root: Thread object
├─ android.os.HandlerThread instance
│    Leaking: NO (PathClassLoader↓ is not leaking)
│    Thread name: 'LeakCanary-Heap-Dump'
│    ↓ Thread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│    Leaking: NO (SingletonImageLoader↓ is not leaking and a ClassLoader is never leaking)
│    ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│    Leaking: NO (SingletonImageLoader↓ is not leaking)
│    ↓ Object[848]
├─ coil3.SingletonImageLoader class
│    Leaking: NO (a class is never leaking)
│    ↓ static SingletonImageLoader.reference
│                                  ~~~~~~~~~
├─ java.util.concurrent.atomic.AtomicReference instance
│    Leaking: UNKNOWN
│    Retaining 74.5 kB in 1244 objects
│    ↓ AtomicReference.value
│                      ~~~~~
├─ coil3.RealImageLoader instance
│    Leaking: UNKNOWN
│    Retaining 74.5 kB in 1243 objects
│    ↓ RealImageLoader.options
│                      ~~~~~~~
├─ coil3.RealImageLoader$Options instance
│    Leaking: UNKNOWN
│    Retaining 72.2 kB in 1120 objects
│    application instance of sample.view.Application
│    ↓ RealImageLoader$Options.diskCacheLazy
│                              ~~~~~~~~~~~~~
├─ kotlin.SynchronizedLazyImpl instance
│    Leaking: UNKNOWN
│    Retaining 71.8 kB in 1106 objects
│    ↓ SynchronizedLazyImpl.initializer
│                           ~~~~~~~~~~~
├─ sample.view.Application$$ExternalSyntheticLambda0 instance
│    Leaking: UNKNOWN
│    Retaining 71.8 kB in 1105 objects
│    f$0 instance of sample.view.MainActivity with mDestroyed = true
│    ↓ Application$$ExternalSyntheticLambda0.f$0
│                                            ~~~
╰→ sample.view.MainActivity instance
     Leaking: YES (ObjectWatcher was watching this because 
sample.view.MainActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
     Retaining 71.8 kB in 1104 objects
     key = a7a62ff2-b675-4cde-9d5a-247b35f00715
     watchDurationMillis = 7644
     retainedDurationMillis = 2641
     key = f5552ff9-fa48-4f70-be4d-12531e2fb29a
     watchDurationMillis = 5176
     retainedDurationMillis = 170
     mApplication instance of sample.view.Application
     mBase instance of androidx.appcompat.view.ContextThemeWrapper

After

LeakCanary is running and ready to detect memory leaks.
Watching instance of androidx.fragment.app.FragmentManagerViewModel (androidx.fragment.app.FragmentManagerViewModel received ViewModel#onCleared() callback) with key 29533f7f-cd32-4385-8bce-608a9ec0e6d1
Watching instance of leakcanary.internal.ViewModelClearedWatcher (leakcanary.internal.ViewModelClearedWatcher received ViewModel#onCleared() callback) with key ae688c5b-26f9-47e8-817c-f6a525faf13e
Watching instance of androidx.lifecycle.SavedStateHandlesVM (androidx.lifecycle.SavedStateHandlesVM received ViewModel#onCleared() callback) with key 0088de13-b1c6-47b1-ac8f-78f307ece06b
Watching instance of androidx.lifecycle.ReportFragment (androidx.lifecycle.ReportFragment received Fragment#onDestroy() callback) with key 06ecef8e-8016-48d8-b929-f02497a404bf
Watching instance of sample.view.MainActivity (sample.view.MainActivity received Activity#onDestroy() callback) with key f898107e-959f-407a-9f9b-b0e1d7fe50a3

Testing Procedure

  1. Created following reproducer original code:
override fun newImageLoader(context: PlatformContext): ImageLoader {
        return ImageLoader.Builder(this)
            .diskCache {
                DiskCache.Builder()
                    .directory(context.getExternalFilesDir("coil")!!.toOkioPath())
                    .build()
            }
            .logger(if (isDebuggable) DebugLogger() else null)
            .build()
    }
  1. Put a simple activity setup like this within onCreate:
        super.onCreate(savedInstanceState)

        val imageView = ImageView(this).apply {
            layoutParams = FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT
            )
        }
        setContentView(imageView)

        imageView.load("https://picsum.photos/4000/4000") {
            memoryCachePolicy(CachePolicy.DISABLED)
            diskCachePolicy(CachePolicy.DISABLED)
        }
  1. As per KittenBalls instructions, I completely exited the app via back press before image loaded. And observed the heap dump.

@KittenBall
Copy link
Copy Markdown

Thank you very much for fixing this issue. I believe this PR should have resolved the problem.


AppWatcher.objectWatcher.expectWeaklyReachable(this,"Watching for leaks from MainActivity")

This should be called within Activity.onDestroy. If it is called in onCreate, then after approximately 5 seconds (I'm not entirely sure of the exact duration), if the Activity has not been destroyed, LeakCanary will consider that the Activity has a memory leak.

In fact, if you have integrated LeakCanary, it will automatically perform the detection — there's no need to manually add anything.

So, in practice, you just need to integrate LeakCanary, launch the Activity, and then exit it before the image finishes loading. LeakCanary will then detect any memory leaks. Once the issue is fixed, nothing will be reported.

@nishatoma
Copy link
Copy Markdown
Contributor Author

nishatoma commented Nov 24, 2025

@KittenBall Okay I have tried again this time without adding any watcher manually, and I can confirm there is no more heap dumps when I exit the activity:

LeakCanary is running and ready to detect memory leaks.
Watching instance of androidx.fragment.app.FragmentManagerViewModel (androidx.fragment.app.FragmentManagerViewModel received ViewModel#onCleared() callback) with key 29533f7f-cd32-4385-8bce-608a9ec0e6d1
Watching instance of leakcanary.internal.ViewModelClearedWatcher (leakcanary.internal.ViewModelClearedWatcher received ViewModel#onCleared() callback) with key ae688c5b-26f9-47e8-817c-f6a525faf13e
Watching instance of androidx.lifecycle.SavedStateHandlesVM (androidx.lifecycle.SavedStateHandlesVM received ViewModel#onCleared() callback) with key 0088de13-b1c6-47b1-ac8f-78f307ece06b
Watching instance of androidx.lifecycle.ReportFragment (androidx.lifecycle.ReportFragment received Fragment#onDestroy() callback) with key 06ecef8e-8016-48d8-b929-f02497a404bf
Watching instance of sample.view.MainActivity (sample.view.MainActivity received Activity#onDestroy() callback) with key f898107e-959f-407a-9f9b-b0e1d7fe50a3

I will update the PR description now, thank you!

@KittenBall
Copy link
Copy Markdown

You are welcome, thank you.

@nishatoma
Copy link
Copy Markdown
Contributor Author

@colinrtwhite I need some help to review this when you get the chance ^^

Comment thread coil/src/androidMain/kotlin/coil3/SingletonImageLoader.android.kt Outdated
Comment thread coil/src/commonMain/kotlin/coil3/SingletonImageLoader.kt Outdated
Comment thread coil/src/commonMain/kotlin/coil3/SingletonImageLoader.kt Outdated
@colinrtwhite
Copy link
Copy Markdown
Member

@nishatoma Thanks for implementing this! Left a few comments then we should be good to merge.

@nishatoma
Copy link
Copy Markdown
Contributor Author

@colinrtwhite Thank you so much for the feedback, I have addressed all comments!

@nishatoma nishatoma requested a review from colinrtwhite January 9, 2026 01:28
Copy link
Copy Markdown
Member

@colinrtwhite colinrtwhite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@colinrtwhite colinrtwhite merged commit 0203c2a into coil-kt:main Jan 15, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SingletonImageLoader.Factory newImageLoader‘s parameter should be application context

3 participants