art collector: learning about Android App Links

02 December 2018

Android App Links is a feature designed to reduce friction for users who want to access your content — instead of a user clicking an HTTP link and navigating to your website, Android will redirect them to the same content in your app, for a seamless transition to a native Android experience.

Last month, I implemented Android App links for art collector, a small app which displays a selection of paintings from the Harvard Art Museum API.

It has three main screens:

  • the gallery showing a selection of paintings
  • an artist gallery, showing the paintings created by a single artist
  • a painting screen, showing a single painting
Gallery Artist Gallery Painting
gallery showing mixed collection of paintings artist gallery showing paintings by Maruyama Ōkyo painting by Maruyama Ōkyo

I created a sub-domain, https://art-collector.ataulm.com, for my website. If someone clicks on a link starting with that hostname, then it should open the art collector app!

If someone clicks on a link to specific content (e.g. https://art-collector.ataulm.com/22730) the app should navigate to that content.

Let’s say:

  • https://art-collector.ataulm.com will open the app at the main gallery
  • https://art-collector.ataulm.com/{artist_id} will open an artist gallery
  • and https://art-collector.ataulm.com/{artist_id}/{painting_id} will open a specific painting

How do we handle in-app navigation?

art collector is modularised by feature — there’s a single base module, app, which has the code for the main gallery, and there are two feature modules (artist and painting) which depend on app.

Since the main gallery (in app) doesn’t know about the artist gallery screen, and doesn’t know about the painting screen, we have to cheat a little to navigate:

fun artistGalleryIntent(artistId: String): Intent {
    val uri = Uri.Builder()
        .scheme(SCHEME)
        .authority(AUTHORITY)
        .path(artistId)
        .build()

    return Intent(Intent.ACTION_VIEW)
        .setClassName(
            BuildConfig.APPLICATION_ID,
            "com.ataulm.artcollector.artist.ui.ArtistActivity"
        )
        .setData(uri)
}

We can use the component name of a given activity to navigate to it without a reference to the class itself. At the time, I thought this was going to be a temporary hack.

I wanted to navigate only with URLs (hence the Uri format to send the artistId as part of the path); as far as the base feature module is concerned, there are no other modules, and it’s just linking out to external websites.

That was the dream.

In reality, it’s not feasible because Android only has limited support for regex in android:pathPattern. Specifically, it can’t exclude patterns, so host/{artist_id}/{painting_id} and host/{artist_id} would be equivalent—in our case, we want to associate these with distinct activity classes.

So actually, we could navigate internally with the component names and intent extras, no need for URIs:

private const val INTENT_EXTRA_ARTIST_ID = "${BuildConfig.APPLICATION_ID}.ARTIST_ID"

fun Intent.artistId() = getStringExtra(INTENT_EXTRA_ARTIST_ID)

fun artistGalleryIntent(artistId: String): Intent {
    return Intent(Intent.ACTION_VIEW)
        .setClassName(
            BuildConfig.APPLICATION_ID,
            "com.ataulm.artcollector.artist.ui.ArtistActivity"
        )
        .putExtra(INTENT_EXTRA_ARTIST_ID, artistId)
}

I raise this to highlight that, internally, your app can do whatever it likes to navigate between screens.

The first step to achieving our goal (frictionless navigation from an external weblink into our app) is associating art collector with the URLs we want to capture.

This PR adds a new DeepLinkActivity which will declare support for all the art-collector.ataulm.com links. It checks the complete Uri in its onCreate(...) function and navigates to the correct screen.

screen recording showing a click on an art collector web link followed by a disambiguation dialog asking whether to open the link in art collector or Google Chrome

Support for deep links gets us pretty close to our goal!

As shown in the above recording, the user is going to be faced with a “disambiguation dialog” — not quite the frictionless experience we’re after.

The difference between deep links and Android App Links is that the latter will automatically open the app without showing the dialog.

For this to work, Android needs to know that our app is allowed to consume the links it’s trying to consume — that is, we need to verify that we own the domain.

The verification is simple in theory — we generate a file, and upload it to our website to prove that we own it.

On the Statement List Generator, we fill in the hosting site domain and the app package name:

screenshot of the statement list generator

and we can generate the app package fingerprint with the Java keytool using our app signing key:

$ keytool -list -v -keystore my-signing-key.keystore

Once we upload the file to our website (https://art-collector.ataulm.com/.well-known/assetlinks.json) then we’re almost good-to-go.

One final thing that we need to do is add android:autoVerify="true":

<activity android:name=".DeepLinkActivity">
    <intent-filter android:autoVerify="true">
        ...

Check out the full PR on GitHub.

and a recording of Android App Links in action:

recording of click on art-collector.ataulm.com link to open “View on the Hudson” in the app

What’s next?

Android App Links is a feature that’s available on Android 6.0 devices and higher. The implementation shown above won’t break on older devices — it will just offer users the disambiguation dialog to choose between art collector and Google Chrome.

Does it make sense to add Android App Links? If the app can render the same content as the website and offer a better experience, then absolutely! But don’t force users into the app if it’s not as good.

If you have any questions about this post, please write below, ask me on Twitter, or feel free to comment on any of the pull requests on art collector!