Get applicationContext from a KMM module
Asked Answered
W

4

9

I'm currently developing a simple KMM module, which needs Context in order to perform some operations. I am aware of ways to achieve that with extending Application class and making dependency injection. What am I trying to do right now - to make this module available out of the box with no need to modify manifest or make manual injection on start up. I am just wondering is it a bad practice to make something like so:

@SuppressLint("StaticFieldLeak")
object SomeUtil {

    private val context = Activity().applicationContext

}

Since applicationContext return the Context for the whole app and we're initializing it once will there be a leak? Or are there some other points not to make it?

Maybe there are some other possibilities to get app context from the module? I've seen some examples of retrieving it from threads, but as I understand this will be (or is already) deprecated.

UPD: This causes an error. Activity() seems to be null. So any ideas how to achieve that without DI and "MyApplication"?

Weingarten answered 19/2, 2021 at 0:2 Comment(0)
D
11

This is a common problem in android libraries - how to get the app context without having access to the application codebase? It's why you often init libraries with something like SharedPrefHelper.init(applicationContext) in your Application.onCreate()

As KMM shared code is a library you get a similar problem. Android app startup is an androidx lib built to solve this (as well as improving startup performance).

Rough sample (everything in shared code):

// In androidMain
class MySqlDelightInitialiser : Initializer<SqlDriver> {
    override fun create(context: Context): SqlDriver {
        val driver = createDriver(context)
        MyLibraryObject.init(context, driver)
        return driver
    }

    override fun dependencies(): List<Class<out Initializer<*>>> {
        return emptyList()
    }
}

// In androidMain/AndroidManifest
<application>
    <provider
        android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.sql-delight-initialiser"
        android:exported="false"
        tools:node="merge"
        tools:replace="android:authorities"
        >
        <meta-data
            android:name="my.package.SqlDelightInitialiser"
            android:value="androidx.startup"
            />
    </provider>
</application>

Descendible answered 2/3, 2021 at 23:35 Comment(3)
Looks like you are abusing the library and misusing it. Why "create()" returns instance of "SqlDriver"? Who is gonna use it under the hood? If some methods returns non-void, that means someone should or is using it. If that is the case, we have to use that instance not do something like this "MyLibraryObject.init(context, driver)".Barleycorn
I edited the sample code with the above suggestionDescendible
Thank you for the edit but few things here. "return Nothing" will throw exception. Also changing from "X" to "Nothing" does not mean we solved the problem :) There must be a reason the Start-Up library returns type instead of void. So if it returns type that means someone should or does use that instance but who and why? Or if that "who" is consumer then how do that consumer gets that instance?Barleycorn
G
6

This is the simple approach I use.

I created a simple AppContext object in the androidModule

internal object AppContext {
    private lateinit var application: Application

    fun setUp(context: Context) {
        application = context as Application
    }

    fun get(): Context {
        if(::application.isInitialized.not()) throw Exception("Application context isn't initialized")
        return application.applicationContext
    }
}

Created an AppContextInitializer and used AndriodX App startup to provide the context to the AppContext.

internal class AppContextInitializer : Initializer<Context> {
    override fun create(context: Context): Context {
        AppContext.setUp(context.applicationContext)
        return AppContext.get()
    }
    override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

You will need to create an AndroidManifest file directly under the androidModule

<manifest xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    >

    <application>
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge"
            >
            <meta-data
                android:name="com.sample.sharedmodule.AppContextInitializer"
                android:value="androidx.startup"
                />
        </provider>
    </application>
</manifest>

To use the application context in your AndroidModule, you just need to call

AppContext.get()
Grison answered 4/9, 2023 at 11:12 Comment(0)
G
5

Well, I'd start with saying this isn't really a KMM question. This only applies to the Android code.

As far as I know, no, there's no way to statically, globally get access to the application context without some semi-hacky solutions. This is a long standing problem that doesn't really have a good solution.

Crashlytics does (did?) something weird by registering a ContentProvider who's only purpose is to get the application and make that available. Assuming you're publishing as an aar, it would register the ContentProvider for you.

https://firebase.googleblog.com/2016/12/how-does-firebase-initialize-on-android.html

I wouldn't recommend that. I greatly prefer configuring library context init myself, but you can try the ContentProvider route.

Gdynia answered 19/2, 2021 at 16:38 Comment(2)
When browsing web I've also found this, which also seems like things muggles should not do. So I'm also looking in DI, which is a bit disappointingWeingarten
"Well, I'd start with saying this isn't really a KMM question." How does having different arguments for different platforms is Android specific and not related to KMM?Brucine
W
1

Short answer: Inject it in constructor or as method param:

class SomeUtil(private val context: Context) {
   ....
}

object SomeUtil {
   fun someMethod(context: Context) { .... }
}

Context (but also Activity, Application, Service) instances are created and destroyed by Android framework and creating instances manually (or mocking) will probably work at compile time, but they will cause exceptions at runtime

Woe answered 19/2, 2021 at 16:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.