[Huawei Developers] turkayavci:
Introduction​

Hello all,

As you’ve probably know already, Huawei has released its own mobile services as the HMS (Huawei Mobile Services) platform and thankfully these services are also supported on third party SDKs such as Flutter, React-Native, Cordova, Xamarin and more.

View attachment 5331737

In this article we will dive in to some of the HMS Flutter Plugins to enhance an imaginary online restaurant app called Forks & Spoons with useful features. We will integrate Huawei Crash, Push and Account Flutter Plugins into our app to meet certain use cases in the first part of these series. Before we begin let me introduce Forks & Spoons to you.

Forks & Spoons​

Forks & Spoons is a a local restaurant that is having a hard time because of Covid-19 epidemic like most of us. The precautions for the epidemic has prohibited the restaurant to accept customers so the owner has started online food delivery in order to reach to its customers and save its business.

View attachment 5331755

We will now integrate Huawei’s Mobile Services to the online food delivery app for the purpose of enriching the user experience and providing useful features with ease. Before we begin I would like to mention that I won’t go into details about the UI code and some business logic to not make this article too long but you can find all the source code in the github repository.

If you are ready let’s begin by integrating the first service to the app.

Account Kit​

Let’s add the Sign In function to authenticate users with their HUAWEI ID. First we create an AccountAuthParamsHelper object to set the fields we need in the response and we pass this object to the AccountAuthService.signIn method. Furthermore we can verify the user’s token after we have obtained the id from the sign in method.

/// Signing In with HUAWEI ID
void _signIn(BuildContext context) async {
// This parameter is optional. You can run the method with default options.
final helper = new AccountAuthParamsHelper();
helper
..setIdToken()
..setAccessToken()
..setAuthorizationCode()
..setEmail()
..setProfile();
try {
AuthAccount id = await AccountAuthService.signIn(helper);
log("Sign In User: ${id.displayName}");
// Optionally verify the id.
await performServerVerification(_id.idToken);
// ..
// ...
// Rest of the business logic.
// ....
} on PlatformException catch (e, stacktrace) {
log("Sign In Failed!, Error is:${e.message}");
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("Could not log in to your account."),
backgroundColor: Colors.red,
));
}
}

/// You can optionally verify the user’s token with this method.
Future<void> performServerVerification(String idToken) async {
var response = await http.post(
"https://oauth-login.cloud.huawei.com/oauth2/v3/tokeninfo",
body: {'id_token': idToken});
print(response.body);
}

We connected the _signIn method above to a user icon button on the app bar and once the user signs in successfully their first and last name will be displayed instead of the icon. Let’s see it in action:

View attachment 5331773

View attachment 5331771

It is a high possibility that you would need the id information again on some part of the app. In Forks&Spoons we had needed it in the drawer widget which displays the user’s shopping cart. In order to obtain the already signed in Huawei ID information we call the AccountAuthManager.getAuthResult method.

void _getAuthResult() async {
try {
AuthAccount _id = await AccountAuthManager.getAuthResult();
log(_id.givenName.toString());
// ..
// ...
// Rest of business logic.
} catch (e, stacktrace) {
log("Error while obtaining Auth Result, $e");
// ..
// ...
// Error handling.
}
}

We let people sign in to our app but there is no way to sign out yet. Let’s fix this by adding a sign out function.

void _signOut() async {
try {
final bool result = await AccountAuthService.signOut();
log("Signed out: $result");
} on PlatformException catch (e, stacktrace) {
log("Error while signing out: $e");
}
}

AGC Crash​

The next service that we are going to integrate to our app is the AppGallery Connect Crash Service which provides a powerful yet lightweight solution to app crash problems. With the service, we can quickly detect, locate, and resolve app crashes, and have access to highly readable crash reports in real time.

We can catch and report all non-fatal exceptions of our app to the AGC Crash Service by adding the following code to our main method.

void main() {
// Obtains an instance of AGCCrash.
AGCCrash _agcCrashInstance = AGCCrash.instance;

// Defines Crash Service's [onFlutterError] API as Flutter's.
FlutterError.onError = _agcCrashInstance.onFlutterError;

// Below configuration records all exceptions that occurs in your app.
runZonedGuarded<Future<void>>(() async {
runApp(MyApp(_agcCrashInstance));
}, (Object error, StackTrace stackTrace) {
AGCCrash.instance.recordError(error, stackTrace);
});
}

Manually handling and reporting errors to AGC Crash Service​

View attachment 5331757

Meet Jerry, The Forks&Spoons is his favorite restaurant and he orders from the app occasionally. Unfortunately his wifi is not working when he opened the app today and he tries to log in to the app. Below is the source code for the login process, notice that we are catching the error and reporting it to the crash service by the recordError method. If you use the try / catch block the thrown exception will not be catched by the configuration we did earlier, so we have to handle it ourselves.

void _signIn() async {
// This parameter is optional. You can run the method with default options.
final helper = new HmsAuthParamHelper();
helper
..setIdToken()
..setAccessToken()
..setAuthorizationCode()
..setEmail()
..setProfile();
try {
_id = await HmsAuthService.signIn(authParamHelper: helper);
log("Sign In User: ${_id.displayName}");
// ..
// ...
// Rest of the business logic.
} on PlatformException catch (e, stacktrace) {
widget.agcCrash.recordError(e, stacktrace);
log("Sign In Failed!, Error is:${e.message}");
}
}

Let’s see what happens when Jerry tries to log in to the app.

View attachment 5331751

Ooops something went wrong. Let’s see the crash report on the AppGallery Connect. Be sure to add the Exception filter to see the exception reports.

View attachment 5331739

Here is the exception report; we can check the stack trace, device information and more in this page, isn’t it cool ?

View attachment 5331741

We can also test an exception by just throwing one and sending its report on the catch clause.

/// Test method for sending an exception record to the agcrash service.
void sendException() {
try {
// Throws intentional exception for testing.
throw Exception("Error occured.");
} catch (error, stackTrace) {
// Records the occured exception.
AGCCrash.instance.recordError(error, stackTrace);
}
}

Furthermore we can set a userId or any custom key value pair to be sent in the crash/exception report.

// Gets AAID for user identification.
String userAAID = await hmsAnalytics.getAAID();

// Sets AAID as user ID.
AGCCrash.instance.setUserId(userAAID);

// Sets user name as custom key while sending crash reports.
AGCCrash.instance.setCustomKey("userName", _id.displayName);

Apart from exceptions Flutter can also have build phase errors or what i like to call widget errors. To catch all of these errors you can use ErrorWidget.builder class along with the crash kit. I have also returned a custom error widget to be rendered on error. Spoiler alert: it contains ducks.

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
builder: (BuildContext context, Widget widget) {
Widget error = Container(
height: double.infinity,
width: double.infinity,
child: Text(
'...rendering error...',
style: TextStyle(color: Colors.white),
),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/duck.jpg'),
repeat: ImageRepeat.repeat,
)),
);
if (widget is Scaffold || widget is Navigator)
error = Scaffold(body: Center(child: error));
ErrorWidget.builder = (FlutterErrorDetails errorDetails) {
print("Widget Error Occurred");
AGCCrash.instance
.recordError(errorDetails.exception, errorDetails.stack);
return error;
};
return widget;
},
title: 'Forks&Spoons',
theme: ThemeData(
primarySwatch: Colors.grey,
),
home: MyHomePage(
hmsAnalytics: hmsAnalytics,
),
);
}

View attachment 5331777

Push Kit​

Let’s move on to one of the most indispensable services for the app: Push Kit. Push Kit is a messaging service provided for you to establish a messaging channel from the cloud to devices. By integrating Push Kit, you can send messages to your apps on user devices in real time. This helps you maintain closer ties with users and increases user awareness of and engagement with your apps.

Before we start to send messages we need to obtain a push token. We can achieve this by setting a callback to the token stream and requesting a token by the Push.getToken method.

void initPush() async {
if (!mounted) return;
// Subscribe to the streams.
Push.getTokenStream.listen(_onTokenEvent, onError: _onTokenError);

// Get the push token.
Push.getToken("");
}

void _onTokenEvent(String token) {
log("Obtained push token: $token");
}

void _onTokenError(Object error) {
PlatformException e = error;
print("TokenErrorEvent: " + e.message);
}

After we add the initPush function to a stateful widget’s initState method in our app we should be able see the push token on the logs.

View attachment 5331765

This token will be very useful while sending messages to our app. Let’s start with a push notification message.

Sign In to the AppGallery Connect and select your project, then select Grow /Push Kit on the side bar and click the Add Notification button. The page below will be displayed.

View attachment 5331759

Let’s quickly prepare a push notification for our belowed users. You can see the message preview on the right side.

View attachment 5331761

To send this push message to our users we can tap the Test Effect button and enter the push token we obtained earlier. We can also send this message to all users by pressing the Submit button above.

View attachment 5331763

We have successfully sent our first push message to our app but it still seems somewhat flat. We can spice things up by adding a deep link and navigating the users to the content directly. This would empower the UX of our app.

Before we jump into the code first, we need to add the intent filter below to the project’s AndroidManifest.xml file inside activity tag. Here we are using the scheme “app” but if you are going to release your app it’s better to change it to something related to your domain name.

<activity>
<!-- Other Configurations -->
<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>

For the implementation let’s return to the initPush function again and add a callback to the intent stream of Push kit. Here we can also use the same callback for handling the startup intent. This intent is included in the notification that opens your app from scratch.

void initPush() async {
if (!mounted) return;
Push.setAutoInitEnabled(true);

// Subscribe to the streams. (Token, Intent and Data Message)
Push.getTokenStream.listen(_onTokenEvent, onError: _onTokenError);
Push.getIntentStream.listen(_onNewIntent, onError: _onIntentError);

// Handles startup intents.
_onNewIntent(await Push.getInitialIntent());

// Get the push token.
Push.getToken("");
}

Now for the best part let’s go back to the AppGallery Connect again and add a button with custom intent uri string to the notification we’ve prepared earlier.

View attachment 5331769

View attachment 5331767

Let’s press the button on the notification and see the deep linking in action.

View attachment 5331745

​

Offering discounts using data messaging​

Some features of your app may require you to send data messages up or down (App to Server or Server to App). By the help of data messaging from Push Kit this operation becomes very easy to achieve.

The Forks&Spoons restaurant’s owner is very generous and he wants to occasionally give discounts to the loyal customers like Jerry who orders from the app oftenly. To implement this we will go back to the initPush method again but this time we will add a callback to the onMessageReceived stream.

void initPush() async {
if (!mounted) return;
Push.setAutoInitEnabled(true);

// Subscribe to the streams. (Token, Intent and Data Message)
Push.getTokenStream.listen(_onTokenEvent, onError: _onTokenError);
Push.getIntentStream.listen(_onNewIntent, onError: _onIntentError);
_onNewIntent(await Push.getInitialIntent());

Push.onMessageReceivedStream
.listen(_onMessageReceived, onError: _onMessageReceiveError);

// Get the push token.
Push.getToken("");
}

And for the data message callback, a function that displays a discount dialog and sets the discount value to the state is enough to cover our feature.

void _onMessageReceived(RemoteMessage remoteMessage) {
String data = remoteMessage.data;
if (remoteMessage.dataOfMap.containsKey("discount")) {
setState(() {
discount = int.parse(remoteMessage.dataOfMap['discount']);
MyHomePage.discount = discount;
});
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => AlertDialog(
title: Text(
"Discount for your pocket, Best food for your stomach!",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
"Congrats you have received a ${remoteMessage.dataOfMap['discount']}% discount.",
textAlign: TextAlign.center,
),
SizedBox(
height: 15,
),
MaterialButton(
onPressed: () => Navigator.pop(context), child: Text("OK"))
],
)),
);
}
log("onRemoteMessageReceived Data: " + data);
}

void _onMessageReceiveError(Object error) {
PlatformException e = error;
log("onRemoteMessageReceiveError: " + e.message);
}

Now we head over to the AppGallery Connect and open the Push Kit Console again. This time we will send a data message that includes a discount value.

View attachment 5331743

View attachment 5331749

Oh a very generous fifty percent discount, Nice! Jerry will be walking on air. I can see him smiling with hunger. Let’s check the discounted prices.

View attachment 5331747

Jerry is excited to order with the reduced prices but he got a text message and he forgot to order, Let us remind him that he left his meal in the cart by using the local notification feature of the Push Kit Plugin.

Did you forget your meal in the cart feature​

To implement this feature we need to check the user’s cart before the app goes to background or killed state. To monitor the app state we can use the WidgetsBindingObserver class from Flutter SDK. But before that we need to add the permissions that are required to send a scheduled local notification to our AndroidManifest.xml file.

<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

For the implementation, first we mix the WidgetBindingObserver to our widget’s state, then add the observer as on the code below to the initState function and override the didChangeAppLifecycleState callback from the WidgetBindingObserver mixin.

Inside the callback we are sending a scheduled local notification to 1 minute after the app is paused. If the user opens the app again we are cancelling all the scheduled notifications.

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.inactive:
log('appLifeCycleState inactive');
break;
case AppLifecycleState.resumed:
log('appLifeCycleState resumed');
Push.cancelScheduledNotifications();
break;
case AppLifecycleState.paused:
log('appLifeCycleState paused');
if (MyHomePage.userChoices.length > 0 &&
state == AppLifecycleState.paused) {
log('Sending sceduled local notification.');
Push.localNotificationSchedule({
HMSLocalNotificationAttr.TITLE: 'You forgot your meal in the cart.',
HMSLocalNotificationAttr.MESSAGE:
"It is better for your meal to stay in your stomach rather than the cart.",
HMSLocalNotificationAttr.FIRE_DATE:
DateTime.now().add(Duration(minutes: 1)).millisecondsSinceEpoch,
HMSLocalNotificationAttr.ALLOW_WHILE_IDLE: true,
HMSLocalNotificationAttr.TAG: "notify_cart"
});
}
break;
case AppLifecycleState.detached:
log('appLifeCycleState suspending');
break;
}
}
// ..
// ...
// Rest of the widget.
}

View attachment 5331753

Tips and Tricks​

Some exceptions and crash reports may be send after the app is opened again, to test an app crash you can use the testIt() method of AGCrash Kit.

Do not listen the streams more than once before disposing them properly, if you try to subscribe to a stream twice the app would throw an error.

Conclusion​

We have authenticated our users, communicated them with messages and gave them discounts whilst monitoring the errors that may occur in the app. By the help of HMS Flutter Plugins all of these features were a lot easy to implement.

We are finished the part 1 of our series but the story will continue since we should also take care of the delivery flow. Apart from that the owner of the restaurant thinks that we can learn one or two things from Jerry by analysing his user behavior in the app.

My colleague Serdar Can will discuss about all these topics on the second part of our article. Thank you and congratulations on finishing the first part. If you have any questions related to article, feel 100% free to ask them in the comments section.

You can also check References and Further Reading section for more information about the Huawei Mobile Services and HMS Flutter Plugins.

See you next time!

References and Further Reading​

Huawei Push Kit Flutter Plugin Development Guide

AG Crash Service Development Guide

Huawei Flutter Plugins Pub Dev Repository

Forks&Spoons App Github Repository

​