Blog Infos
Author
Published
Topics
Published

Photo by SpaceX on Unsplash

 

Let’s assume you are working on Kotlin Multiplatform project.

You need to retrieve the GPS position of the user from common code, and there’s no library out there that does so.

That’s when you decide to write a new Kotlin Multiplatform library that abstracts GPS location on both Android and iOS, because you’re working on a mobile app.

Since you’re short on creativity, you name it KLocationManager.

You start planning how the public API exposed by the library will look. Maybe something like this:

private val kLocation: KLocationManager = KLocationManager()

fun showLocationUpdates() {
    scope.launch {
        kLocation.observeLocation().collect {
            showMessage("Got (${it.lat}, ${it.lng})")
        }
    }
}

Then you start investigating how to actually retrieve a GPS position stream on the two mobile platforms.

iOS Implementation

On iOS, all looks simple enough. There’s a class CLLocationManger that you can initialize, attach a delegate (a callback to receive updates), and call startUpdatingLocation().

val locationManager = CLLocationManager()

fun observeLocation() = callbackFlow<KLocation> {
    var mDelegate: CLLocationManagerDelegateProtocol = object : CLLocationManagerDelegateProtocol, NSObject() {
        override fun locationManager(
            manager: CLLocationManager,
            didUpdateLocations: List<*>
        ) {
            val locations = didUpdateLocations.map { it as CLLocation }
            if (locations.isNotEmpty()) {
                locations.last().coordinate.useContents { 
                    trySend(this.toKLocation())
                }
            }
        }
    }

    locationManager.apply {
        delegate = mDelegate
        startUpdatingLocation()
    }

    awaitClose {
        locationManager.stopUpdatingLocation()
    }
}
Android Implementation

On Android, you’d like to use FusedLocationProviderClient which is simple enough for the use case. But you notice something’s wrong starting from the first line:

val fusedLocationClient =
    LocationServices.getFusedLocationProviderClient(context)
The Context issue

As you might already have guessed, we’re writing a multiplatform library here, and we already need from the outside something very platform dependant: the Android Context.

You may ask Android users to pass you some context by calling an init method before using the library:

KLocationManager.init(context)

That, anyway, will add some complexities when using the library in another multiplatform project, since users will be required to add a platform-specific code just to call the init method when on Android.

On StackOverflow, there are also other hacky solutions like those, where developers try to expose a different constructor for each platform, create a common Context class (that does nothing on iOS and it’s an actual Context on Android), use DI, and more…

But is there a solution that can handle all of this internally to the library and does not require effort from users, so they can use the same exact API on both platforms and in common code?

The Jetpack App Startup solution

App Startup is a library part of Jetpack useful to initialize components of your (Android) library as soon as the app starts (as you might have guessed from the name).

This could be useful to us because:

  • it automatically runs the initialization steps before any library code (so we’re safe that our components will be always initialized before use);
  • is Android only: no need to touch a thing in the iOS module
  • it exposes a method to get the Android Context in the initialization phase.

The last reason is exactly why it could be useful to us since we’re looking for a way to inject an Android Context into our library code without bothering the user.

Since App Startup will be required on the Android module only, we can define it as a dependency of androidMain:

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Kotlin Multiplatform- From “Hello World” to the Real World

By now you’ve surely heard of Kotlin Multiplatform, and maybe tried it out in a demo. Maybe you’ve even integrated some shared code into a production app.
Watch Video

Kotlin Multiplatform- From “Hello World” to the Real World

Russell Wolf
Kotlin Multiplatform
Touchlab

Kotlin Multiplatform- From “Hello World” to the Real World

Russell Wolf
Kotlin Multiplatform ...
Touchlab

Kotlin Multiplatform- From “Hello World” to the Real World

Russell Wolf
Kotlin Multiplatform
Touchlab

Jobs

val androidMain by getting {
    dependencies {
        implementation("androidx.startup:startup-runtime:lastVersion")
        [...]
}

Check here which is the last published version of the library.

AppStartup will invoke at startup a special class that implements the Initializer interface.

public interface Initializer<T> {

    /**
     * Initializes and a component given the application {@link Context}
     *
     * @param context The application context.
     */
    @NonNull
    T create(@NonNull Context context);

    /**
     * @return A list of dependencies that this {@link Initializer} depends on. This is
     * used to determine initialization order of {@link Initializer}s.
     * <br/>
     * For e.g. if a {@link Initializer} `B` defines another
     * {@link Initializer} `A` as its dependency, then `A` gets initialized before `B`.
     */
    @NonNull
    List<Class<? extends Initializer<?>>> dependencies();
}

The first method is actually interesting to us, note that by implementing this interface, we get a free Context in our app.

The second method, instead, is useful when your library depends on other lib initializers, so you can specify the order in which the app runs the initializers at startup. We can ignore this for our use case.

So, in our example we’re going to retrieve the Context from create() method and store it somewhere we can access it later.

import android.content.Context
import androidx.startup.Initializer

internal lateinit var applicationContext: Context
    private set

public object KLocationContext

public class KLocationInitializer: Initializer<KLocationContext> {
    override fun create(context: Context): KLocationContext {
        applicationContext = context.applicationContext
        return KLocationContext
    }

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

Note that KLocationInitializer.kt will be in the android/platform-specific module and not in our library common code.

Finally, App Startup uses a special content provider to discover and call all the component initializers. An entry in AndroidManifest.xml will make our Initializer discoverable.

<application>
    <provider
        android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge">

        <meta-data  android:name="dev.paolorotolo.klocation.KLocationInitializer"
            android:value="androidx.startup" />
    </provider>
    [...]
</application>

We can safely use our library’s AndoridManifest.xml, since it will be eventually merged with the main Manifest of the app using it.

And we’re done!

Each time we require a Context on the Android module, we can actually access the variable applicationContext that has been initialized automatically.

The Android location code will now look something like this:

// injected context here from App Startup
val fusedLocationClient 
    = LocationServices.getFusedLocationProviderClient(applicationContext)

fun observeLocation(): Flow<KLocation> = callbackFlow {
    val locationCallback = object: LocationCallback(){
        override fun onLocationResult(locationResult: LocationResult) {
            super.onLocationResult(locationResult)
            trySend(locationResult)
        }
    }

    fusedLocationClient.requestLocationUpdates(
        buildLocationRequest(),
        locationCallback,
        Looper.getMainLooper()
    ).addOnFailureListener { close(it) }

    awaitClose {
        fusedLocationClient.removeLocationUpdates(locationCallback)
    }
}.map { it.asKLocation() }
Conclusion

Our personal context injector made with App Startup will:

  • run on Android only (since androidx-startup a dependency of androidMain)
  • make applicationContext available only in the Android-specific code module (since it’s in the same module of KLocationInitializer.kt)
  • be transparent to the users of the library, which will invoke the same API observeLocation() in common code and in all the supported targets.

Additional tip: if you want a ready-to-use solution that implements this under the hood without writing any additional code, check out Louis CAD’s Splitties App Context library.

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
I love Swift enums, even though I am a Kotlin developer. And iOS devs…
READ MORE
blog
After successfully implementing the basic Kotlin multiplatform app in our last blog, we will…
READ MORE
blog
Kotlin Multiplatform despite all of its benefits sometimes has its own challenges. One of…
READ MORE
blog
I develop a small multi-platform(android, desktop) project for finding cars at an auction using…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu