[Huawei Developers] John Wo:
In this article I will show the basics of deep linking for Flutter, using Huawei Push Kit Plugin along with uni_links package.

Here are the links for those packages:

https://pub.dev/packages/huawei_push

https://pub.dev/packages/uni_links

Deep Linking

The most basic definition for a deep link is: “A link that sends users to related content on an application”.

Okay but why is this important ?

For improving the User Experience (UX) of course. By utilizing custom uniform resource identifiers (URIs), developers can create funnels in their apps for landing users to the specific content and make the user experience better.

We can validate this with an example: An e-commerce application on your phone has sent you a notification that there will be a discount on stickers. Would you prefer going to the stickers page by tapping the notification or rather navigate by yourself through the enourmous menus like Home > Products > Handmade Products > Stationery & Party Supplies > Stationery Stickers. I am assuming you would choose the first aproach.

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922083853.81923893128066716596630328764339:50511019062801:2800:841FF95C40BEB4EEE6B7C9DA9D273CF38E362DEDB2F1230476CD6761F14A33A9.jpg"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922083853.81923893128066716596630328764339:50511019062801:2800:841FF95C40BEB4EEE6B7C9DA9D273CF38E362DEDB2F1230476CD6761F14A33A9.jpg[/IMG]

Huawei Push Kit allows developers to send push notifications that can include custom intents or actions. This is well suited for our case. So let’s get started for the sake of our users’ experience.

Before we begin, there are prerequisites that need to be completed.

Huawei Developer Account: you must have this account to use the Push Kit Service. Click here to sign up if you don’t have an account already.

HMS Core SDK setup: For using the Push Kit we have to make some configurations on Huawei Developer Console and in our application. Refer to this medium post for installation and if you have any trouble doing so you can also check this post for a more in-depth setup.

The project

The project will be a very simple app that will display information about Huawei Mobile Services. Here is the project’s Github link if you want to follow from there.

Project Setup

As I’ve mentioned before we will use uni_links and Huawei Push Kit plugins in our project. We will also add flutter_webview_plugin into the mix for displaying the website content of the service. So let’s start by adding these to our pubspec.yaml file

dependencies:
flutter:
sdk: flutter
huawei_push: 4.0.4+300
uni_links: 0.4.0
flutter_webview_plugin: 0.3.11

To listen for intents, uni_links package needs a configuration in the AndroidManifest.xml file. Add an intent filter inside <activity> tag like below.

<application
<!-- . . .
Other Configurations
. . . -->
<activity/>
<!-- . . .
Other Configurations
. . . -->
<!-- Add the intent filter below.(inside the application and activity tags) -->
<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="app"/>
</intent-filter>
</activity>
</application

Here we have used a scheme called “app”. You can also use your own custom scheme. For more information on this subject refer to this document.

Now that we are done with the installation let’s get started with coding. The project is very simple, you can check the file hierarchy below.

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922083938.34378452724553343374978318925142:50511019062801:2800:DCE7CFDA49FF10319A14BF8115C404D5E636640D11F2B41FC696DDC2E9AE31C6.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922083938.34378452724553343374978318925142:50511019062801:2800:DCE7CFDA49FF10319A14BF8115C404D5E636640D11F2B41FC696DDC2E9AE31C6.png[/IMG]

We have two app pages. Home page will show various Huawei Mobile Services and the content page will display the service information inside a webview. We will navigate to this page if a notification with custom intent is tapped.

Main.dart

import 'package:deep_linking_demo/router.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Deep Linking Demo',
theme: ThemeData(
primarySwatch: Colors.red,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
onGenerateRoute: Router.generateRoute,
initialRoute: '/',
);
}
}

This main.dart file is almost identical with what you get for default except for the named route generation. Here, I have defined the onGenerateRoute and initialRoute properties of the MaterialApp and I have passed Router’s generateRoute method. Let’s look at the router.dart file to see what’s going on.

Router.dart

I used named routes because they contain less boilerplate and easier to use with custom intents. For using the named routes in Flutter we should set the names for our routes and return the corresponding MaterialPageRoute based on the name. Here, I am also getting the arguments needed for the content page and passing to its widget. (We put these arguments when we call the navigator)

import 'package:deep_linking_demo/screens/content_page.dart';
import 'package:deep_linking_demo/screens/home_page.dart';
import 'package:flutter/material.dart';
class Router {
static const String HomePageRoute = '/';
static const String ContentPageRoute = '/ContentPage';
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case HomePageRoute:
return MaterialPageRoute(builder: (context) => HomePage());
case ContentPageRoute:
final ContentPageArguments args = settings.arguments;
return MaterialPageRoute(
builder: (context) => ContentPage(
serviceName: args.serviceName,
serviceUrl: args.serviceUrl,
),
);
default:
// Error the named route doesn't exist
return MaterialPageRoute(builder: (context) => HomePage());
}
}
}

Hms.dart

This class’ mere purpose is to hold the data for the names and URLs for the HMS services that will be displayed on our home page.

class HMS {
final String name;
final String url;
final HMSGroup hmsGroup;
const HMS(this.name, this.url, this.hmsGroup);
static const Map<HMSGroup, List<HMS>> HMSMap = {
HMSGroup.AI : _aiServicesList,
HMSGroup.SECURITY : _securityServicesList,
// ...Rest of the mappings
};
static const List<HMS> _aiServicesList = [
HMS('ML Kit','https://developer.huawei.com/consumer/en/hms/huawei-mlkit',HMSGroup.AI),
HMS('HUAWEI HiAI Foundation','https://developer.huawei.com/consumer/en/hiai#Foundation',HMSGroup.AI),
HMS('HUAWEI HiAI Engine','https://developer.huawei.com/consumer/en/hiai#Engine',HMSGroup.AI),
HMS('HUAWEI HiAI Service','https://developer.huawei.com/consumer/en/hiai#Service',HMSGroup.AI)
];
static const List<HMS> _securityServicesList = [
HMS('FIDO','https://developer.huawei.com/consumer/en/hms/huawei-fido',HMSGroup.SECURITY),
HMS('Safety Detect','https://developer.huawei.com/consumer/en/hms/huawei-safetydetectkit/',HMSGroup.SECURITY)
];
// ...Rest of the list definitions
}
enum HMSGroup {
APP_SERVICES,
MEDIA,
GRAPHICS,
AI,
SMART_DEVICE,
SECURITY,
SYSTEM
}
extension HMSGroupExtension on HMSGroup {
String get text {
switch (this) {
case HMSGroup.APP_SERVICES:
return "App Services";
case HMSGroup.MEDIA:
return "Media";
case HMSGroup.GRAPHICS:
return "Graphics";
case HMSGroup.AI:
return "AI";
case HMSGroup.SMART_DEVICE:
return "Smart Device";
case HMSGroup.SECURITY:
return "Security";
case HMSGroup.SYSTEM:
return "System";
default:
return "Other";
}
}
}

I’ve deleted some of the definitions to not bother you with details. Check the github repo for the full code.

Home_page.dart

This is the widget that the most important functions occur so I will split into parts and get into some details for a better explanation.

You can refer to this part for creating your own custom intent navigations for the purpose of deep linking.

Obtaining a push token

For sending push notifications we need to obtain a push token for our device.

Under the state of the widget, define an EventChannel that will listen for the push token and a string variable to hold the token.

class _HomePageState extends State<HomePage> {
String _token = '';
static const EventChannel TokenEventChannel =
EventChannel(Channel.TOKEN_CHANNEL);
. . .
}

Initialize functions below for obtaining the push token.

Future<void> initPlatformState() async {
if (!mounted) return;
TokenEventChannel.receiveBroadcastStream()
.listen(_onTokenEvent, onError: _onTokenError);
await Push.getToken();
}
void _onTokenEvent(Object event) {
// This function gets called when we receive the token successfully
setState(() {
_token = event;
});
print('Push Token: ' + _token);
}
void _onTokenError(Object error) {
setState(() {
_token = error;
});
print(_token);
}

Call the initPlatformState function on the widget’s initState method. You should now see your token printed on the debug console.

@override
void initState() {
super.initState();
initPlatformState();
}

Obtaining a push token is the most crucial part when using the push kit. If you run into any errors while obtaining the token here is checklist that could help:

1. Check your SHA-256 signature and package name on the Huawei Developer Console

2. Make sure the Push Kit is enabled on the console (Manage APIs tab)

3. Whenever you change something on the console, download the agconnect-services.json file again.

Deep linking

We will need two functions to listen for the custom intents: One is for when our app is on foreground (active) and the other one is for when the app is not active and it is opened by an intent.

Future<Null> initLinkStream() async {
if (!mounted) return;
_sub = getLinksStream().listen((String link) {
print(link);
var uri = Uri.dataFromString(link);
String page = uri.path.split('://')[1];
String serviceName = uri.queryParameters['name'];
String serviceUrl = uri.queryParameters['url'];
Navigator.of(context).pushNamed(
page,
arguments: ContentPageArguments(serviceName, serviceUrl),
); // Navigate to the page from the intent
}, onError: (err) {
print("Error while listening for the link stream: " + err.toString());
});
}
Future<void> initInitialLinks() async {
// Platform messages may fail, so we use a try/catch PlatformException.
try {
String initialLink = await getInitialLink();
print(initialLink ?? 'NO LINK');
if (initialLink != null) {
print(initialLink);
var uri = Uri.dataFromString(initialLink);
String page = uri.path.split('://')[1];
String serviceName = uri.queryParameters['name'];
String serviceUrl = uri.queryParameters['url'];
try {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Navigator.of(context).pushNamed(
page,
arguments: ContentPageArguments(serviceName, serviceUrl),
); // Navigate to the page from the intent
});
} catch (e) {
Push.showToast(e);
}
}
} on PlatformException {
print('Error: Platform Exception');
}
}

Call these functions on the widgets initState

@override
void initState() {
super.initState();
initPlatformState();
initInitialLinks();
initLinkStream();
}

Our custom intent inside the push notification looks like this:

app:///ContentPage?name=Push Kit&url=https://developer.huawei.com/consumer/en/hms/huawei-pushkit

By adding query params, we can utilize the Uri class’ queryParameters method to easily obtain the values we need and not worry about string parsing.

Now for the final part of home_page.dart here is the UI code below.

Widget serviceButton(HMS service) {
return ListTile(
title: Text(service.name),
onTap: () => Navigator.of(context).pushNamed(
'/ContentPage',
arguments: ContentPageArguments(service.name, service.url),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Huawei Mobile Services"),
centerTitle: true,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
height: MediaQuery.of(context).size.height * 0.8,
child: ListView.builder(
itemCount: HMS.HMSMap.length,
itemBuilder: (context, idx) => ExpansionTile(
title: Text(
'${HMS.HMSMap.keys.elementAt(idx).text}',
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 25),
),
children: HMS.HMSMap[HMS.HMSMap.keys.elementAt(idx)]
.map((e) => serviceButton(e))
.toList(),
),
),
)
],
),
);
}

Content_page.dart

The last file is content_page.dart. This widget is very simple since it’s only purpose is to display the related service content inside a webview.

import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
class ContentPageArguments {
final String serviceName;
final String serviceUrl;
ContentPageArguments(this.serviceName, this.serviceUrl);
}
class ContentPage extends StatelessWidget {
final String serviceName;
final String serviceUrl;
ContentPage({Key key, @required this.serviceName, @required this.serviceUrl})
: super(key: key) {
assert(serviceName != null);
assert(serviceUrl != null);
}
@override
Widget build(BuildContext context) {
return WebviewScaffold(
url: serviceUrl,
appBar: AppBar(
title: Text(serviceName),
),
withZoom: true,
withLocalStorage: true,
hidden: true,
initialChild: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Center(
child: Text('Loading.....'),
),
SizedBox(
height: 10,
),
Center(child: CircularProgressIndicator())
],
),
),
);
}
}

Sending Push Notifications with Custom Intent

Now for the last part let’s head over to Huawei Developer Console and create a push notification that include a custom intent. Enter the details like in the image below and press “Test Effect” button or submit your push notification from top right corner.

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922084749.41828590070115708845493978590215:50511019062801:2800:66D65AFC3D9C71273146A6A85A1FC5C47F8D30BEB27A6C7EBCE0552A77156996.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922084749.41828590070115708845493978590215:50511019062801:2800:66D65AFC3D9C71273146A6A85A1FC5C47F8D30BEB27A6C7EBCE0552A77156996.png[/IMG]

You can find the custom intent uri entered here on the deep linking section of this article

If you press “Test Effect” button console will prompt you to enter the token you obtained earlier.

Enter the push token obtained earlier in the app

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922084722.59982910296464187214986539678596:50511019062801:2800:4D447DA7E12675514114BA60846AD657FCC7E5BC6E66CC017D4B2B0217CEBE9E.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922084722.59982910296464187214986539678596:50511019062801:2800:4D447DA7E12675514114BA60846AD657FCC7E5BC6E66CC017D4B2B0217CEBE9E.png[/IMG]

Deep Linking while app is on foreground.

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922084623.29492142654304468766108360502788:50511019062801:2800:FCC5DAAFA4A570827B71EF9913B4871DC53F5266DF478029D601C2AF96B7F004.gif"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922084623.29492142654304468766108360502788:50511019062801:2800:FCC5DAAFA4A570827B71EF9913B4871DC53F5266DF478029D601C2AF96B7F004.gif[/IMG]

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922084834.92239299038593801639404509693471:50511019062801:2800:2AA5F5D80DFE4302A0A9536343034E553C0D82D5CFE164665DF43DDC822A6C10.gif"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/006/578/685/2640852000006578685.20200922084834.92239299038593801639404509693471:50511019062801:2800:2AA5F5D80DFE4302A0A9536343034E553C0D82D5CFE164665DF43DDC822A6C10.gif[/IMG]

Deep linking is working as expected. Kudos to you if you got this far!

Conclusion

Push notifications and deep linking is a must for a mobile application nowadays since their use can boost up user retention and experience if used properly. Huawei Push Kit’s notifications include custom intent and custom action features for the use of deep linking but they aren’t limited with these ones only. If you want to check all the features click here.

I hope this tutorial was helpful for you. If you have any questions regarding this article feel free to ask them on the comment section.

You can also check our other articles about using Huawei Mobile Services on Flutter below.

https://medium.com/huawei-developers/integrating-huawei-analytics-kit-to-flutter-projects-and-sending-events-3dcc4c4f03f

https://medium.com/huawei-developers/using-huawei-map-kit-on-flutter-applications-f83b2a5668bc

https://medium.com/huawei-developers/sending-push-notifications-on-flutter-with-huawei-push-kit-plugin-534787862b4d

Reference

Github demo project: https://github.com/HMS-Core/hms-flutter-plugin/tree/master/flutter-hms-push