Blog Infos
Author
Published
Topics
, , ,
Published
Photo by Marc Sendra Martorell on Unsplash

In the previous article about app architecture, I covered how to build your DomainData, and Presentation layers. If you haven’t read it yet, I would strongly recommend doing that so we can be on the same page.

Today I’ll show you how we can embrace this architecture adding deep link support to your app.

I’m not going to basics, I’m assuming you are already familiar with deep links. I want to show you how you can architecture it.

Android platform provides native support for deep links in the Android SDK. Popular frameworks like Fragment Navigation and Compose Navigation provide API to work with deep links. The one common problem that API has, is they are all platform-dependent. As we have learned about architecture, it is better to decouple logic from platform dependencies as much as possible. That is what I’m going to do today.

Let’s start with the Domain layer and create GetDeeplinkUseCase.

class GetDeeplinkUseCase(
private val deeplinkRepository: DeeplinkRepository,
) {
suspend operator fun invoke(uri: String): DeepLink {
val deepLink = deeplinkRepository.getDeepLink(parsedUri)
return deepLink
}
}

Here you can see a simple use case that takes URI as input and return DeepLink object as a result. We also need a repository that knows how to parse URI and return deep link types that we know how to work with.

interface DeeplinkRepository {
suspend fun getDeepLink(uri: String): DeepLink
}

The DeepLink is just an interface in the Domain layer.

view raw DeepLink.kt hosted with ❤ by GitHub

All the code above we put in the shared module in our app. The implementation of these interfaces will be in the app module, which knows everything in our app and brings together all feature modules in the app.

The DeepLinkRepository implementation will be in the data layer in app module.

class DeeplinkDataRepository(
private val localDataSource: AppDeepLinkLocalDataSource,
) : DeeplinkRepository {
override suspend fun getDeepLink(uri: String): DeepLink {
return localDataSource.getDeepLinkData(uri)
}
}

As you can see it contains local DataSoure for deep links. There also can be a remote one, if you want to keep all the knowledge about deep links on your server, but let’s stop for a moment and define what type of deep links we can face in our project.

The Deep Links Type

The first and most common deep link is the regular URL that we can open with our app and navigate the user to the right screen with the needed state based on data from the deep link.

https://moove.com/ticket/confirmation?ryderId=4g5g4&price=43434

For example, with the URL above we can parse and understand that this is the confirmation screen for buying tickets and we can get ryderId and price from it. Also, we can open this URL in the browser and get to the confirmation page.

The second type is with the custom scheme.

moove://app/confirmation?ryderId=4g5g4&price=43434

It has the same structure as a regular URL but with a custom scheme moove and custom path. This is equivalent to the Web URL above but can be understood only by the app. There is often a case when you don’t have a web page equivalent to the app screen, but the business wants to use deep links in the marketing campaigns and put users on the screen they want. In that case, it’s a good solution to create a unique deep link to your project and use it for that need.

As I’ve mentioned in the marketing campaigns it’s time to introduce the third type of deep link. The Deferred link or Dynamic link. This is the link that masks the original one and can be parsed only with third-party services, for instance, Firebase dynamic link, and Adjust Deferred link.

https://moove.page.link/45hj45j

Usually, it’s the short custom URL created by the third-party services that know how to parse it. You can use this URL in your marketing campaigns and when the user clicks on it, it will open the app and deliver the original deep link.

For example, when the user clicks on such a link the app opens and we get this URL as input.

https://moove.page.link/45hj45j

By the scheme, we can understand that this is the deferred link, and use the third-party services SDK to parse it and get the original one. In our case, it will be the next deep link.

moove://app/confirmation?ryderId=4g5g4&price=43434

This is the link we know how to parse into the app readable form.

Now is the right time to back to our local DataSource of deep links and take a look at the code.

class AppDeepLinkLocalDataSource(
private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
companion object {
private const val RYDER_ID = "ryderId"
private const val PRICE = "price"
const val HOME = "moove://app/home"
const val FARE_LIST = "moove://app/fare_list"
const val CONFIRM_CONFIRMATION = "/ticket/confirmation"
const val MOOVE_CONFIRM_CONFIRMATION = "moove://app/confirmation"
}
suspend fun getDeepLinkData(uri: String): DeepLink = withContext(backgroundDispatcher) {
when {
uri.matchesPattern(CONFIRM_CONFIRMATION) -> {
val innerUri = URI.create(uri)
val params = getQueryParams(innerUri)
AppDeepLink.Confirmation(
ryderId = params[RYDER_ID]!!,
fare = Fare(
description = "",
price = params[PRICE]?.toFloat()!!
),
)
}
uri.matchesPattern(MOOVE_CONFIRM_CONFIRMATION) -> {
val innerUri = URI.create(uri)
val params = getQueryParams(innerUri)
AppDeepLink.Confirmation(
ryderId = params[RYDER_ID]!!,
fare = Fare(
description = "",
price = params[PRICE]?.toFloat()!!
),
)
}
uri.isThat(FARE_LIST) -> {
val innerUri = URI.create(uri)
val params = getQueryParams(innerUri)
AppDeepLink.FareList(ryderId = params[RYDER_ID]!!)
}
uri.isThat(HOME) || uri.matchesPattern(HOME) -> AppDeepLink.Home
else -> AppDeepLink.Unknown
}
}
private fun String.isThat(type: String): Boolean {
/**
* Handle two cases with slash symbol at the end and without it
* app/home/ and app/home
*/
return contains(type, ignoreCase = true)
}
private fun getQueryParams(url: URI): Map<String, String> {
val query = url.query ?: return emptyMap()
return query
.split("&".toRegex())
.filter { it.isNotEmpty() }
.map(::mapQueryParameter)
.associateBy(keySelector = { it.first }, valueTransform = { it.second })
}
private fun mapQueryParameter(query: String): Pair<String, String> {
val index = query.indexOf("=")
val key = if (index > 0) query.substring(0, index) else query
val value = if (index > 0 && query.length > index + 1) {
query.substring(index + 1)
} else null
return Pair(
URLDecoder.decode(key, StandardCharsets.UTF_8.name()),
URLDecoder.decode(value, StandardCharsets.UTF_8.name())
)
}
}

A lot of code as you can see. Take your time to understand what’s going on there and I will help you with that. The AppDeepLinkLocalDataSource is the source of truth in our app, that knows how to parse all supported deep links in the app. Here are a few things I want to highlight:

  • I’m using java.net.URI instead of android.net.Uri to be able to cover the code with Junit tests and run it on the JVM.
  • The AppDeepLink is the sealed class that implement DeepLink interface from the Domain shared module.
view raw AppDeepLink.kt hosted with ❤ by GitHub

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

So, the deep link the app knows how to work with is just a class containing the data needed to open the right screen in the correct state. It’s simple but at the same time, this is the abstraction we need to decouple logic from URLs and not spread it across the code base.

Basically, AppDeepLinkLocalDataSource is just a parse that knows how to extract data from URLs.

There is no parsing logic for dynamic links. Let’s add it. First, we need to create another use case responsible for getting the original deep link from the dynamic one.

class GetDynamicLinkUseCase(
private val dynamicLinkRepository: DynamicLinkRepository,
) {
suspend operator fun invoke(uri: String): String? {
return dynamicLinkRepository.parseLink(uri)
}
}

The input is dynamic links and the result is the original one as String.

interface DynamicLinkRepository {
suspend fun parseLink(uri: String): String?
}

The implementation of it in the app module.

class DynamicLinkDataRepository(
private val dataSource: FirebaseDynamicLinkDataSource,
) : DynamicLinkRepository {
override suspend fun parseLink(uri: String): String? {
return dataSource.parseLink(uri)
}
}

As you can see we have FirebaseDynamicLinkDataSource as the component that knows how to extract the original deep link.

class FirebaseDynamicLinkDataSource(
private val host: String,
private val firebaseDynamicLinks: FirebaseDynamicLinks,
private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
suspend fun parseLink(uri: String): String? = withContext(backgroundDispatcher) {
if (uri.matchesPattern(host).not()) return@withContext null
try {
firebaseDynamicLinks.getDynamicLink(Uri.parse(uri)).await().link?.toString()
} catch (e: Exception) {
throw DynamicLinkParseException(cause = e)
}
}
}

Here we delegate to the Firebase SDK parsing logic as the Firebase only knows how to do that.

The host is the base scheme of the URL. For us it’s moove.page.link part. if the URL doesn’t match it then we return null because we don’t know how to work with them.

Let’s back to GetDeeplinkUseCase and add logic with dynamic links.

class GetDeeplinkUseCase(
private val deeplinkRepository: DeeplinkRepository,
private val getDynamicLinkUseCase: GetDynamicLinkUseCase,
) {
suspend operator fun invoke(uri: String): DeepLink {
val parsedUri = getDynamicLinkUseCase(uri) ?: uri
val deepLink = deeplinkRepository.getDeepLink(parsedUri)
return deepLink
}
}

By default, we assume the URL is the dynamic link and try to parse it.

  • If we get the result null then understand that this is the deep link we already know how to parse it and pass it to the parser.
  • If we get the parsed link from GetDynamicLinkUseCase then we use it as the link we know how to work with.

So, now we defined the Data and Domain layer in deep link architecture, it’s time to move forward to the Presentation layer.

In the shared module of the app in the Presentation layer we need to create DeepLinkNavigator.

interface DeepLinkNavigator {
fun navigateTo(link: DeepLink)
}

The navigator has only one method to execute navigation and takes as DeepLink as input. The implementation of this navigator will be in the app module

class DeepLinkAppNavigator(
private val globalAppNavigator: GlobalAppNavigator,
private val ticketsNavigator: TicketsNavigator,
) : DeepLinkNavigator {
override fun navigateTo(link: DeepLink) {
when (link) {
is AppDeepLink.FareList -> ticketsNavigator.goFares(link.ryderId)
is AppDeepLink.Confirmation -> {
ticketsNavigator.goFares(link.ryderId)
ticketsNavigator.goToConfirmation(
ryderId = link.ryderId,
fareDescription = link.fare.description,
farePrice = link.fare.price
)
}
is AppDeepLink.Home, AppDeepLink.Unknown -> globalAppNavigator.goHome()
}
}
}

Here you can see different navigators. If you feel confused about what is it, I recommend you to read the previous article about the Presentation layer. the chapter about navigation.

The DeepLinkAppNavigator contains all navigators it needs, from different feature modules in the app. For example the TicketNavigator knows how to navigate to the ticket screens.

The TicketNavigator the interface we keep in the shared module

https://gist.github.com/88b0ea6a30932febdc2d28ff6fdaa94

The ticket the module depends on the shared, we can use it for navigation though ticket screens. Also, we have the GlobalAppNavigator that knows how to navigate to any place in the app. It implements TicketNavigator.

interface GlobalAppNavigator : ScreenNavigator, TicketsNavigator {
fun goHome()
}

In the app module we implement it as AppNavigator, the instance of it we inject as dependencies following the Dependency Inversion Principle (DIP).

class AppNavigator(
private val navController: NavController,
) : GlobalAppNavigator {
override fun goFares(ryderId: String) {
navController.navigateSafely(HomeFragmentDirections.actionHomeFragmentToTicketsFlow())
navController.navigateSafely(
RyderListFragmentDirections.actionRydersFragmentToFareListFragment(ryderId = ryderId)
)
}
override fun goToConfirmation(ryderId: String, fareDescription: String, farePrice: Float) {
navController.navigateSafely(
FareListFragmentDirections.actionFareListFragmentToConfirmationFragment(
ryderId = ryderId,
fare = FareModel(
description = fareDescription,
price = farePrice
)
)
)
}
override fun goBack() {
navController.navigateUp()
}
override fun goHome() {
navController.navigateSafely(
MobileNavigationDirections.actionGlobalGoHome()
)
}
}
view raw AppNavigator.kt hosted with ❤ by GitHub

So, we have covered the navigator part and it’s time to look at the entry point in our app — MainActivity. This activity will start when the user hits the URL.

class MainActivity : AppCompatActivity() {
private val mainViewModel: MainActivityViewModel by viewModel()
/* ... */
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
mainViewModel.handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
mainViewModel.handleIntent(intent)
}
/* ... */
}
view raw MainActivity.kt hosted with ❤ by GitHub

The ViewModel will extract the URL from the intent and pass it to the GetDeeLinkUseCase.

https://gist.github.com/2881ce23588b9d44263726790f14d68

And don’t forget to set intent-filter in the AndroidManifest so your app could recognize different types of deep links.

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Supported schemes -->
<data android:scheme="moove" />
<!-- Corporate subdomains -->
<data android:host="app" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Supported schemes -->
<data android:scheme="https" />
<data android:scheme="http" />
<!-- Corporate subdomains -->
<data android:host="moove.com" />
<data android:pathPattern="/ticket/confirmation" />
<data android:pathPattern="/home" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Supported schemes -->
<data android:scheme="https" />
<data android:scheme="http" />
<!-- Corporate subdomains -->
<data android:host="${firebaseDynamicLinkHost}" />
</intent-filter>

Finally, we have covered the main part, but there is one more thing I want to share with you.

The remote deep link parser

Imagine the case when your app doesn’t know anything about how to parse the deep link, and all this logic is on your server side. When your app gets the URL and asks the server to parse it for me. What’s the point of doing that? You can ask me.

The business often uses Web URLs as deep links too, it’s practical to have one URL to represent the same page and screen in the app. The problem comes when the business decides to do SEO optimization of your website. In the process of doing that, the URL that represents the web page can be changed. If you have all your parsing logic locally in your app, it will stop working correctly. To fix that you need to make changes in your client apps and release the new version. It’s not a flexible and easy-to-break solution.

To avoid such a situation I recommend you make your server the main source of truth, that knows how to parse any link and send the response, the client will always understand. When you need to change the web page URL, change the parsing logic on the server too, as a result, all your clients will remain working.

Wrapping up

Today, I showed you how you can add deep link support to your app using the Clean Architecture approach in your mind. Make deep link platform independent and easy to cover with tests.

You can find all the code and the sample app here.

Stay tuned for the next App Architecture topic to cover.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu