9

I am trying to implement the following singleton pattern: SingletonClass.getInstance(context).callMethod()

While there are a variety of tutorials that explain how to make singletons in Kotlin, none of them address the fact that holding a context in a static field will cause memory leaks in Android.

How do I create the above pattern without creating a memory leak?

Update:

Here is my implementation of CommonsWare's solution #2. I used Koin.

Singleton class:

class  NetworkUtils(val context: Context) {

}

Application Class:

class MyApplication : Application() {

    val appModule = module {
        single { NetworkUtils(androidContext()) }
    }

    override fun onCreate() {
        super.onCreate()
        startKoin(this, listOf(appModule))
    }
}

Activity class:

class MainActivity : AppCompatActivity() {

    val networkUtils : NetworkUtils by inject()

}
Nicola Gallazzi
  • 7,897
  • 6
  • 45
  • 64
Foobar
  • 7,458
  • 16
  • 81
  • 161

4 Answers4

14

Option #1: Have getInstance(Context) call applicationContext on the supplied Context and hold that. The Application singleton is created when your process is and lives for the life of the process. It is pre-leaked; you cannot leak it further.

Option #2: Get rid of getInstance() and set up some form of dependency injection (Dagger 2, Koin, etc.). There are recipes for these DI frameworks to have them supply the Application singleton to the singletons that they create and inject downstream.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 1
    Can you give a simple example of Option 2? I read through the Koin documentation and I couldn't figure out how to create a singleton that has access to a `Context` – Foobar Jan 29 '19 at 17:39
  • @Roymunson: https://insert-koin.io/docs/1.0/documentation/reference/index.html#_getting_android_context_inside_a_module – CommonsWare Jan 29 '19 at 17:46
  • Thanks. I added my solution, which uses Koin, to the bottom of my post. Can you take a quick look at it? I've never used dependency injection before, so I don't know if I'm doing it right. If my recipe is correct, will I be able to use it to access `NetworkUtils` in any `Activity` or `Fragment` with a simple `val networkUtils : NetworkUtils by inject()`? – Foobar Jan 29 '19 at 18:17
  • @Roymunson: Ah, I forgot about a documentation gap: `androidContext()` returns the `Application` typed as `Context, but `androidApplication()` returns the `Application` as `Application`. So, I'd use `androidApplication()`, and have `class NetworkUtils(val app: Application)`, to make *really* sure that you have the `Application` singleton. – CommonsWare Jan 29 '19 at 18:22
  • @Roymunson: "will I be able to use it to access NetworkUtils in any Activity or Fragment with a simple val networkUtils : NetworkUtils by inject()?" -- yes, that should work. – CommonsWare Jan 29 '19 at 18:23
  • What is the benefit of using `androidApplication()` and `class NetworkUtils(val app: Application)` instead of using `androidContext()` and `class NetworkUtils(val context: Context)`. Since `context` is no longer a static field, it does not seem to matter what type of `context` the class `NetworkUtils` has access to. – Foobar Jan 29 '19 at 18:31
  • @Roymunson: Well, the objective was to avoid the memory leak. *Presumably*, Koin has no bugs, and `androidContext()` really does return the `Application`. Personally, I like to add the extra type safety, to make damn sure I am getting the `Application`. And while `context` is not `static`, Koin is holding the `NetworkUtils` (`single`), and therefore the `NetworkUtils` cannot be garbage collected. – CommonsWare Jan 29 '19 at 18:33
0

When you call the getInstance() for the first time, the Context that you pass to this function is saved forever. So, the context in further getInstance() calls doesn't have anything to do there. I never save this Context.

This is what I am doing:

Create an object in Kotlin and initialize the object with a context as soon as the app starts. Instead of storing the context, I perform whichever operation is required with that context.

object PreferenceHelper {

    private var prefs: SharedPreferences? = null

    fun initWith(context: Context){
        if(prefs == null) this.prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
    }

    fun someAction(){ .... }
}

and inside the Application class:

class MyApp: Application(){
   override fun onCreate(){
      PreferenceHelper.initWith(this)
   }
 }

and later anywhere in the app:

 PreferenceHelper.someAction()

You can do this if you don't need a reference to the Context every time you perform something with the Singleton class.

Vishnu Haridas
  • 7,355
  • 3
  • 28
  • 43
0

I would not store the context in the SingletonClass, I would simply pass the context to each method of the class via dependency injection. Something like:

SingletonClass.callMethod(context)

Define the "static" method in the companion object like that:

 companion object {
        fun callMethod(context: Context) {
            // do Something
        }
    }

Then call it from your activity with:

SingletonClass.callMethod(this)

Hope that it helps :)

Nicola Gallazzi
  • 7,897
  • 6
  • 45
  • 64
-1

if You must create singleton class with context involved, you can do it like this. This will help. In this case, your context will be reset in every activity, when you call getInstance(context).

public class MyClass {

    private Context context;

    public static getInstance(Context context){
         if(instance ==null)
             instance = new MyClass();
         instance.setContext(context);
         return instance;
    }

   public void setContext(Context context){
      this.context = context;
  }
}
rgaraisayev
  • 398
  • 3
  • 13
  • That won't actually solve his problem. In fact it would make it worse, as now you're majorly broken if the user hits back or you have a Service. – Gabe Sechan Jan 28 '19 at 20:09