Deep Linking in Android with Kotlin and Clean Architecture: A Comprehensive Guide
Hello, devs I am your App Sensei and Today, we’re going to discuss deep linking in Android, specifically using Kotlin and following the principles of Clean Architecture. Deep linking is a powerful feature that allows you to direct users to specific content within your app from external sources, such as websites, social media, or even other apps. By the end of this article, you’ll be equipped with the knowledge to implement deep links in a clean, maintainable, and scalable way. So grab your favorite snack, and let’s get started! 🍿🚀
What is Deep Linking?
Deep linking refers to the practice of using a URL to link directly to a specific page or content within a mobile application. This is particularly useful for directing users to specific content or features, providing a seamless user experience. There are two main types of deep links in Android:
Traditional Deep Links: These links open your app to a specific screen if the app is installed. If the app isn’t installed, they might show an error or redirect the user to the app store.
Deferred Deep Links: These links can direct users to specific content even if the app is not installed. When the user installs the app, they are taken directly to the linked content.
Universal Links (App Links): These are the next level of deep links that not only open your app when installed but also open your website if the app isn’t installed. They are more secure and are automatically verified by Android.
Project Setup
Before writing the code, make sure your Android project is set up with the necessary dependencies.
Add the Required Dependencies:
- Add dependencies for navigation components and Koin (for dependency injection) to your
build.gradle
files.
- Add dependencies for navigation components and Koin (for dependency injection) to your
// Project-level build.gradle
buildscript {
dependencies {
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.7.3'
}
}
// App-level build.gradle
apply plugin: 'androidx.navigation.safeargs.kotlin'
dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:2.7.3"
implementation "androidx.navigation:navigation-ui-ktx:2.7.3"
implementation "org.koin:koin-android:3.4.2"
}
Set Up Navigation Component:
- Create a navigation graph (
nav_graph.xml
) in theres/navigation/
directory to define the navigation paths within your app.
- Create a navigation graph (
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.example.app.ui.home.HomeFragment"
android:label="Home"
tools:layout="@layout/fragment_home">
<action
android:id="@+id/action_homeFragment_to_detailsFragment"
app:destination="@id/detailsFragment" />
</fragment>
<fragment
android:id="@+id/detailsFragment"
android:name="com.example.app.ui.details.DetailsFragment"
android:label="Details"
tools:layout="@layout/fragment_details" />
</navigation>
Implementing Deep Linking
1. Data Layer
In the data layer, we’ll create a repository that handles the deep linking logic.
DeepLinkRepository.kt: This interface and its implementation take care of managing deep links in the app. It includes methods to understand deep link URLs and connect them to specific destinations or actions. The implementation breaks down links, checks the URL structure, extracts parameters, and triggers the right navigation action. By centralizing this in
DeepLinkRepository
, everything stays neat and scalable.interface DeepLinkRepository { fun getDeepLinkDestination(uri: Uri): DeepLinkDestination? } class DeepLinkRepositoryImpl : DeepLinkRepository { override fun getDeepLinkDestination(uri: Uri): DeepLinkDestination? { return when (uri.path) { "/home" -> DeepLinkDestination.Home "/details" -> DeepLinkDestination.Details(uri.getQueryParameter("itemId")?.toIntOrNull()) else -> null } } }
- DeepLinkDestination.kt: A sealed class called
DeepLinkDestination
lays out all the possible deep link destinations in the app. It helps keep everything organized by clearly listing each destination. For instance, there'sHome
for the home screen andDetails
with anitemId
for specific item details. This approach not only makes the code easier to read but also boosts type safety, which helps cut down on errors and keeps the app running smoothly.
- DeepLinkDestination.kt: A sealed class called
sealed class DeepLinkDestination {
object Home : DeepLinkDestination()
data class Details(val itemId: Int?) : DeepLinkDestination()
}
2. Domain Layer
In this layer, we’ll create use cases that handle the business logic for deep linking.
HandleDeepLinkUseCase.kt: This use case helps direct deep links to the right place in the app. It looks at the URI path and query parameters to guide users to the correct screen. For example, "/home" takes you to the Home screen, and "/details" with an "itemId" brings you to the Details screen for that specific item. This makes navigation smooth and enhances the user experience. By wrapping this logic in a use case, we keep the code tidy and easy to manage.
class HandleDeepLinkUseCase(private val repository: DeepLinkRepository) { fun execute(uri: Uri): DeepLinkDestination? { return repository.getDeepLinkDestination(uri) } }
3. Presentation Layer
In the presentation layer, we handle the UI and user interactions.
DeepLinkViewModel.kt: This ViewModel plays an important role in managing navigation based on deep link information. When a deep link comes in, it uses the
HandleDeepLinkUseCase
to figure out where to go. It checks the URI and query parameters to guide users to the right screen, like Home or a specific Details page. By managing this in the ViewModel, we ensure the UI stays responsive and interactions are smooth, making the user experience with navigation seamless and enjoyable.class DeepLinkViewModel( private val handleDeepLinkUseCase: HandleDeepLinkUseCase ) : ViewModel() { private val _destination = MutableLiveData<DeepLinkDestination?>() val destination: LiveData<DeepLinkDestination?> get() = _destination fun handleDeepLink(uri: Uri) { _destination.value = handleDeepLinkUseCase.execute(uri) } }
MainActivity.kt: In
MainActivity.kt
, the activity catches deep links and kicks off the right navigation flow. It grabs the URI and sends it over toDeepLinkViewModel
, which usesHandleDeepLinkUseCase
to figure out where to go. This setup makes sure we respond accurately to deep links, guiding users to the right screen, whether it's Home or a Details page. This approach keeps our code neat and enhances the user experience with spot-on navigation.class MainActivity : AppCompatActivity() { private lateinit var viewModel: DeepLinkViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this).get(DeepLinkViewModel::class.java) handleDeepLink(intent?.data) observeDestination() } private fun handleDeepLink(uri: Uri?) { uri?.let { viewModel.handleDeepLink(it) } } private fun observeDestination() { viewModel.destination.observe(this, Observer { destination -> destination?.let { when (it) { is DeepLinkDestination.Home -> navigateToHome() is DeepLinkDestination.Details -> navigateToDetails(it.itemId) } } }) } private fun navigateToHome() { findNavController(R.id.nav_host_fragment).navigate(R.id.homeFragment) } private fun navigateToDetails(itemId: Int?) { val action = HomeFragmentDirections.actionHomeFragmentToDetailsFragment(itemId) findNavController(R.id.nav_host_fragment).navigate(action) } }
nav_graph.xml: To handle deep link navigation, update your
nav_graph.xml
to set up navigation paths that deep links will trigger. For each destination, include a<deepLink>
element with theapp:uri
attribute for the URI pattern. Make sure your app's manifest file includes an<intent-filter>
for the activity that hosts the navigation graph, specifying the supported URI schemes and paths. This setup lets users jump straight to specific content in your app.p.<navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" app:startDestination="@id/homeFragment"> <fragment android:id="@+id/homeFragment" android:name="com.example.app.ui.home.HomeFragment" android:label="Home"> <action android:id="@+id/action_homeFragment_to_detailsFragment" app:destination="@id/detailsFragment" /> <deepLink app:uri="myapp://home" /> </fragment> <fragment android:id="@+id/detailsFragment" android:name="com.example.app.ui.details.DetailsFragment" android:label="Details"> <argument android:name="itemId" app:argType="integer" android:defaultValue="0" /> <deepLink app:uri="myapp://details?itemId={itemId}" /> </fragment> </navigation>
Dependency Injection with Koin
To manage our dependencies, we’ll use Koin.
AppModule.kt: Define your Koin module with dependencies.
val appModule = module { single<DeepLinkRepository> { DeepLinkRepositoryImpl() } single { HandleDeepLinkUseCase(get()) } viewModel { DeepLinkViewModel(get()) } }
MyApplication.kt: Start Koin in your application class.
class MyApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MyApplication) modules(appModule) } } }
Testing Deep Linking
Testing deep linking is crucial to ensure that your app correctly handles navigation when a deep link is clicked.
DeepLinkRepositoryTest.kt: Unit test for the repository.
class DeepLinkRepositoryTest {
private val repository = DeepLinkRepositoryImpl()
@Test
fun `getDeepLinkDestination returns Home for home URI`() {
val uri = Uri.parse("myapp://home")
val result = repository.getDeepLinkDestination(uri)
assert(result is DeepLinkDestination.Home)
}
@Test
fun `getDeepLinkDestination returns Details for details URI`() {
val uri = Uri.parse("myapp://details?itemId=1")
val result = repository.getDeepLinkDestination(uri)
assert(result is DeepLinkDestination.Details)
assert((result as DeepLinkDestination.Details).itemId == 1)
}
}
HandleDeepLinkUseCaseTest.kt: Unit test for the use case.
class HandleDeepLinkUseCaseTest { private val repository: DeepLinkRepository = mockk() private val useCase = HandleDeepLinkUseCase(repository) @Test fun `execute should return Home destination for home URI`() { val uri = Uri.parse("myapp://home") every { repository.getDeepLinkDestination(uri) } returns DeepLinkDestination.Home val result = useCase.execute(uri) assert(result is DeepLinkDestination.Home) } @Test fun `execute should return Details destination for details URI`() { val uri = Uri.parse("myapp://details?itemId=1") every { repository.getDeepLinkDestination(uri) } returns DeepLinkDestination.Details(1) val result = useCase.execute(uri) assert(result is DeepLinkDestination.Details) assert((result as DeepLinkDestination.Details).itemId == 1) } }
Customizing Deep Links
You can customize deep links to suit your app's needs. For example, you might want to handle complex URIs or pass additional parameters.
Handling Complex URIs: Modify your repository to parse and handle more complex URIs.
class DeepLinkRepositoryImpl : DeepLinkRepository { override fun getDeepLinkDestination(uri: Uri): DeepLinkDestination? { return when { uri.pathSegments.contains("home") -> DeepLinkDestination.Home uri.pathSegments.contains("details") -> { val itemId = uri.getQueryParameter("itemId")?.toIntOrNull() DeepLinkDestination.Details(itemId) } else -> null } } }
Handling Multiple Actions: You can define multiple actions in your navigation graph for different deep link scenarios.
<fragment android:id="@+id/detailsFragment" android:name="com.example.app.ui.details.DetailsFragment" android:label="Details"> <argument android:name="itemId" app:argType="integer" android:defaultValue="0" /> <deepLink app:uri="myapp://details?itemId={itemId}" /> <deepLink app:uri="myapp://details/view" /> </fragment>
And there you have it—a friendly guide to implementing deep linking in your Android app using Kotlin and Clean Architecture! By following these steps, you can make sure your app’s navigation is smooth for users and easy to manage and grow for developers.
Deep linking is a great way to boost user engagement and direct them to specific content or features in your app. By using Clean Architecture, you can handle this complexity well and keep your code organized.
I hope you found this article useful and feel more confident about adding deep links to your projects. If you have any questions or run into any issues, feel free to reach out. See you in the next blog!
Connect with Me:
Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? 😊 You can also follow my Instagram page for more mobile development-related content. 📲👨💻 Let’s stay connected, share knowledge and have some fun in the exciting world of app development! 🌟