Deep links are links that can trigger a mobile application to open or to be proposed as an option to open that link. Once opened by the deep link, an app should show the screen corresponding to that link.
There are three types of deep links :
Deep links
Deep links are URIs of any scheme that take users directly to a specific part of an app. A deep link is created by adding an intent filter in the AndroidManifest.xml
:
<activity android:name=".MyMapActivity" android:exported="true" ...>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geo" />
</intent-filter>
</activity>
Web links
Web links are deep links that use the HTTP and HTTPS schemes. On Android 12 and higher, clicking a web link (that is not an Android App Link) always shows content in a web browser.
A web link can be created with the intent-filter :
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:host="yourdomain.com" />
</intent-filter>
Android App Links
Android App Links, are links that allow your app to be directly opened whenever a user clicks on it. It's a special case of web links where the intent filter has the attribute autoVerify
set to 'true'.
<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" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="myownpersonaldomain.com" />
</intent-filter>
How to set up deep linking in React Native
we are going to use expo and react-navigation to achieve that.
Setup a simple project
For this purpose let's create a simple project. If you don't want to set everything up manually you can get the full code here: github.com/declaudefrancois/deep-links.
Init a new project.
npx create-expo-app -t blank-typescript <project_name>
cd project_name
Add react navigation (see https://reactnavigation.org/docs/getting-started/)
yarn add @react-navigation/native && npx expo install react-native-screens react-native-safe-area-context
Install the stack navigator
yarn add @react-navigation/native-stack
Your project should look like this :
Now let's set up our screens :
// App.tsx
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomeScreen from "./screens/HomeScreen";
import ItemsScreen from "./screens/ItemsScreen";
import ItemDetailsScreen from "./screens/ItemDetailsScreen";
import { Text } from "react-native";
export type StackParamList = {
Home: undefined;
Items:
| {
// Query params
id?: string;
}
| undefined;
Item: {
id: number;
};
};
const Stack = createNativeStackNavigator<StackParamList>();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Items" component={ItemsScreen} />
<Stack.Screen name="Item" component={ItemDetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
// `/screens/Home.tsx`
import { Button, Pressable, StyleSheet, Text, View } from "react-native";
import React from "react";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { StackParamList } from "../App";
type HomeScreenProps = NativeStackScreenProps<StackParamList, "Home">;
export default function HomeScreen({ navigation }: HomeScreenProps) {
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome to deep-link tutorial !</Text>
<Pressable
onPress={() => navigation.navigate("Items")}
style={({ pressed }) => ({
backgroundColor: pressed ? "#039BE590" : "#039BE5",
padding: 16,
borderRadius: 8,
})}
>
<Text style={{ color: "#fff" }}>Go to Items</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 16,
},
title: {
fontSize: 24,
fontWeight: "bold",
},
});
// `/screens/Items.tsx`
import {
FlatList,
ListRenderItem,
Pressable,
StyleSheet,
Text,
View,
} from "react-native";
import React, { useCallback, useEffect } from "react";
import { StackParamList } from "../App";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
interface Item {
id: number;
title: string;
// Other properties
}
export const ITEMS: Item[] = [
{
id: 1,
title: "Item 1",
},
{
id: 2,
title: "Item 2",
},
{
id: 3,
title: "Item 3",
},
{
id: 4,
title: "Item 4",
},
];
type ItemsScreenProps = NativeStackScreenProps<StackParamList, "Items">;
export default function ItemsScreen({ navigation, route }: ItemsScreenProps) {
const renderItem = useCallback<ListRenderItem<Item>>(({ item }) => {
return (
<Pressable
style={styles.itemContainer}
onPress={() =>
navigation.navigate("Item", {
id: item.id,
})
}
>
<View style={styles.itemImage} />
<View style={styles.detailsSection}>
<Text>Item</Text>
</View>
</Pressable>
);
}, []);
useEffect(() => {
if (route.params?.id) {
navigation.replace("Item", {
id: parseInt(`${route.params.id}`),
});
}
}, [route.params?.id]);
if (route.params?.id) {
return null;
}
return (
<View>
<FlatList
data={ITEMS}
ListHeaderComponent={() => (
<View style={{ marginBottom: 16 }}>
<Text style={{ fontSize: 16, fontWeight: "bold" }}>Items page</Text>
</View>
)}
contentContainerStyle={{ padding: 16, paddingBottom: 100 }}
ItemSeparatorComponent={() => <View style={{ height: 16 }} />}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
/>
</View>
);
}
const styles = StyleSheet.create({
itemContainer: {
backgroundColor: "#fff",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#ccc",
shadowColor: "#000",
shadowOpacity: 0.2,
shadowRadius: 1,
shadowOffset: {
width: 3,
height: 3,
},
elevation: 3,
justifyContent: "space-between",
borderRadius: 16,
},
itemImage: {
width: "100%",
height: 150,
backgroundColor: "#ccc",
borderRadius: 8,
},
detailsSection: {
paddingTop: 8,
},
itemTitle: {
fontSize: 16,
fontWeight: "bold",
},
});
// `/screens/Item.tsx`
import { Button, StyleSheet, Text, View } from "react-native";
import React from "react";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { StackParamList } from "../App";
import { ITEMS } from "./ItemsScreen";
type ItemDetailsScreenProps = NativeStackScreenProps<StackParamList, "Item">;
export default function ItemDetailsScreen({
navigation,
route,
}: ItemDetailsScreenProps) {
const { id } = route.params;
const item = ITEMS.find((item) => item.id === id);
if (!item) {
return (
<View style={styles.container}>
<Text>Item not found</Text>
<Button
title="Go to Items"
onPress={() => {
if (navigation.canGoBack()) {
navigation.goBack();
} else {
navigation.navigate("Items");
}
}}
/>
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.itemImage} />
<Text style={styles.itemTitle}>{item.title} #{item.id}</Text>
<Text>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptates
amet illo officiis animi ipsam dignissimos impedit eos eveniet quisquam
molestiae quidem voluptas commodi ullam minima ut, consequatur maxime
dolor non?
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
padding: 16,
},
itemImage: {
width: "100%",
height: 300,
backgroundColor: "#ccc",
borderRadius: 8,
},
itemTitle: {
fontSize: 16,
fontWeight: "bold",
},
});
Let's set up our app-links
Step 1: Let's create an intent filter
Let's add the following content in our android/app/src/main/AndroidManifest.xml
file, inside our MainActivity as shown in the image below:
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
<data android:scheme="http"/>
<data android:host="yourdomain.com" />
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
Step 2: Publish a digital asset in the domain to verify our app links
The digital asset helps us to set up a trusted relation between your app and your website thus allowing your app to be the default handler for all links to your website.
First Get the SHA-256 signing key for your app
cd android
./gradlew signingReport
The output should be similar to this :
Next, create an assetlinks.json
available at https://yourdomain.com/.well-known/assetlinks.json with the following content :
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yth.deelinks",
"sha256_cert_fingerprints": [
"<Add your SHA-256 keys here>",
]
}
}
]
Then rebuild your app to make the changes take effect :
npx expo run:android
Step 4: Handle the deep links in your app
Install expo-linking
npx expo install expo-linking
And update the App.tsx
file with the following content :
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import * as Linking from "expo-linking";
import HomeScreen from "./screens/HomeScreen";
import ItemsScreen from "./screens/ItemsScreen";
import ItemDetailsScreen from "./screens/ItemDetailsScreen";
import { Text } from "react-native";
const prefix = Linking.createURL("/");
export type StackParamList = {
Home: undefined;
Items:
| {
// Query params
id?: string;
}
| undefined;
Item: {
id: number;
};
};
const Stack = createNativeStackNavigator<StackParamList>();
export default function App() {
const linking = {
prefixes: [prefix, "https://yourdomain.com"],
};
return (
<NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Items" component={ItemsScreen} />
<Stack.Screen name="Item" component={ItemDetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
Step 4: Your website should also be able to handle your app links (optional)
This can be useful for example if your app is not installed on the current device.
Setting up our app links with the expo
We can also let Expo automatically create the intent filters for us, by configuring them in the app.json file.
let's add the property "intentFilters"
in expo["android"] :
{
"expo": {
"android": {
"intentFilters": [
{
"action": "VIEW",
"category": ["BROWSABLE", "DEFAULT"],
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "yourdomain.com"
}
]
}
]
},
}
}
Tell Expo to update our AndroidManifest
and rebuild your app :
npx expo prebuild --platform android && npx expo run:android
Testing the deep links
Using Android Debug bridge
We can use the Android Debug Bridge with the activity manager (am) tool to test our app links :
adb shell am start
-W -a android.intent.action.VIEW
-d <URI> <PACKAGE>
For example :
adb shell am start
-W -a android.intent.action.VIEW
-d "deeplinks://Home" com.yth.deelinks
adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/Item?id=1"
Using with npx uri-scheme
npx uri-scheme open [your deep link] --android
For example
npx uri-scheme open deeplinks://Home --android