How to use Kotlin Flows with Huawei Cloud DB - Huawei Developers

In this article we will talk about how we can use Kotlin Flows with Huawei Cloud DB.
Since both Kotlin Flows and Huawei Cloud DB is really huge topic we will not cover deeply and just talk about general usage and how we can use the two together.
You can refer this article about Kotlin Flows and this documentation for Cloud DB for more and detail explanation.
Kotlin Flows
A flow is an asynchronous version of a Sequence, a type of collection whose values are lazily produced. Just like a sequence, a flow produces each value on-demand whenever the value is needed, and flows can contain an infinite number of values.
Flows are based on suspending functions and they are completely sequential, while a coroutine is an instance of computation that, like a thread, can run concurrently with the other code.
We can create a flow easily with flow builder and emit data
Code:
private fun getData() = flow {
val data = fetchDataFromNetwork()
emit(data)
}
fetchDataFromNetwork is a simple function that simulate network task
Code:
private suspend fun fetchDataFromNetwork() : Any {
delay(2000) // Delay
return Any()
}
Flows are cold which means code inside a flow builder does not run until the flow is collected.
Code:
GlobalScope.launch {
getData().collect {
LogUtils.d("emitted data: $it")
}
}
Collect flow and see emitted data.
Using flow with one-shot callback is easy but what if we have multi-shot callback? In other words, a specified callback needs to be called multiple times?
Code:
private fun getData() = flow {
myAwesomeInterface.addListener{ result ->
emit(result) // NOT ALLOWED
}
}
When we try to call emit we see an error because emit is a suspend function and suspend functions only can be called in a suspend function or a coroutine body.
At this point, Callback flow comes to rescue us. As documentation says
Creates an instance of the cold Flow with elements that are sent to a SendChannel provided to the builder’s block of code via ProducerScope. It allows elements to be produced by code that is running in a different context or concurrently.
Therefore the callback flow offers a synchronized way to do it with the offer option.
Code:
private fun getData() = callbackFlow {
myAwesomeInterface.addListener{ result ->
offer(result) // ALLOWED
}
awaitClose{ myAwesomeInterface.removeListener() }
}
The offer() still stands for the same thing. It's just a synchronized way (a non suspending way) for emit() or send()
awaitClose() is called either when a flow consumer cancels the flow collection or when a callback-based API invokes SendChannel.close manually and is typically used to cleanup the resources after the completion, e.g. unregister a callback.
Using awaitClose()is mandatory in order to prevent memory leaks when the flow collection is cancelled, otherwise the callback may keep running even when the flow collector is already completed.
Now we have a idea of how we can use flow with multi-show callback. Lets continue with other topic Huawei Cloud DB.
Huawei Cloud DB
Cloud DB is a device-cloud synergy database product that provides data synergy management capabilities between the device and cloud, unified data models, and various data management APIs.
Cloud DB enables seamless data synchronization between the device and cloud, and supports offline application operations, helping developers quickly develop device-cloud and multi-device synergy applications
After enable Cloud DB and make initializations, we can start with reading data.
First need a query for getting user data based on given accountId
Code:
val query: CloudDBZoneQuery<User> = CloudDBZoneQuery.where(User::class.java).equalTo("accountId", id)
Then we need to execute this query
Code:
val queryTask: CloudDBZoneTask<CloudDBZoneSnapshot<User>> = cloudDBZone.executeQuery(query, CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_PRIOR)
While executing a query we have to define query policy which define your priority.
POLICY_QUERY_FROM_CLOUD_PRIOR means that Cloud DB will try to fetch data from cloud if it fails it will give cached data if exist. We can also use POLICY_QUERY_FROM_LOCAL_ONLY or POLICY_QUERY_FROM_CLOUD_ONLY based on our use case.
As the last step, add success and failure callbacks for result.
Code:
queryTask
.addOnSuccessListener {
LogUtils.i("queryTask: success")
}
.addOnFailureListener {
LogUtils.e("queryTask: failed")
}
Now let’s combine these methods with callback flow
Code:
@ExperimentalCoroutinesApi
suspend fun getUserData(id : String?) : Flow<Resource<User>> = withContext(ioDispatcher) {
callbackFlow {
if (id == null) {
offer(Resource.Error(Exception("Id must not be null")))
[email protected]
}
// 1- Create query
val query: CloudDBZoneQuery<User> = CloudDBZoneQuery.where(User::class.java).equalTo("accountId", id)
// 2 - Create task
val queryTask: CloudDBZoneTask<CloudDBZoneSnapshot<User>> = cloudDBZone.executeQuery(
query,
CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_PRIOR
)
try {
// 3 - Listen callbacks
offer(Resource.Loading)
queryTask
.addOnSuccessListener {
LogUtils.i("queryTask: success")
// Get user data from db
if (it.snapshotObjects != null) {
// Check item in db exist
if (it.snapshotObjects.size() == 0) {
offer(Resource.Error(Exception("User not exists in Cloud DB!")))
[email protected]
}
while (it.snapshotObjects.hasNext()) {
val user: User = it.snapshotObjects.next()
offer(Resource.Success(user))
}
}
}
.addOnFailureListener {
LogUtils.e(it.localizedMessage)
it.printStackTrace()
// Offer error
offer(Resource.Error(it))
}
} catch (e : Exception) {
LogUtils.e(e.localizedMessage)
e.printStackTrace()
// Offer error
offer(Resource.Error(e))
}
// 4 - Finally if collect is not in use or collecting any data we cancel this channel
// to prevent any leak and remove the subscription listener to the database
awaitClose {
queryTask.addOnSuccessListener(null)
queryTask.addOnFailureListener(null)
}
}
}
Resource is a basic sealed class for state management
Code:
sealed class Resource<out T> {
class Success<T>(val data: T) : Resource<T>()
class Error(val exception : Exception) : Resource<Nothing>()
object Loading : Resource<Nothing>()
object Empty : Resource<Nothing>()
}
For make it more easy and readable we use liveData builder instead of mutableLiveData.value = newValue in ViewModel
Code:
val userData = liveData(Dispatchers.IO) {
getUserData("10").collect {
emit(it)
}
}
In Activity, observe live data and get the result
Code:
viewModel.userData.observe(this, Observer {
when(it) {
is Resource.Success -> {
hideProgressDialog()
showUserInfo(it.data)
}
is Resource.Loading -> {
showProgressDialog()
}
is Resource.Error -> {
// show alert
}
is Resource.Empty -> {}
}
})
Just like one shot request above it is possible to listen live data changes with Cloud DB. In order to do that we have to subscribe snapshot.
Code:
val subscription = cloudDBZone.subscribeSnapshot(query, CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_PRIOR,
object : OnSnapshotListener<User> {
override fun onSnapshot(snapShot: CloudDBZoneSnapshot<User>?, error: AGConnectCloudDBException?) {
// do something
}
})
This callback will be called every time the data is changed.
Let’s combine with callback flow again
Code:
@ExperimentalCoroutinesApi
suspend fun getUserDataChanges(id : String?) : Flow<Resource<User>> = withContext(ioDispatcher) {
callbackFlow {
if (id == null) {
offer(Resource.Error(Exception("Id must not be null")))
[email protected]
}
// 1- Create query
val query: CloudDBZoneQuery<User> = CloudDBZoneQuery.where(User::class.java).equalTo("accountId", id)
// 2 - Register query
val subscription = cloudDBZone.subscribeSnapshot(query, CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_PRIOR, object : OnSnapshotListener<User> {
override fun onSnapshot(snapShot: CloudDBZoneSnapshot<User>?, error: AGConnectCloudDBException?) {
// Check error
if (error != null) {
error.printStackTrace()
offer(Resource.Error(error))
return
}
// Check data
try {
val snapShotObjects = snapShot?.snapshotObjects
// Get user data from db
if (snapShotObjects != null) {
// Check item in db exist
if (snapShotObjects.size() == 0) {
offer(Resource.Error(Exception("User not exists in Cloud DB!")))
return
}
while (snapShotObjects.hasNext()) {
val user : User = snapShotObjects.next()
offer(Resource.Success(user))
}
}
} catch (e : Exception) {
e.printStackTrace()
offer(Resource.Error(e))
} finally {
snapShot?.release()
}
}
})
// 3 - Remove subscription
awaitClose {
subscription.remove()
}
}
}
From now on we can listen data changes on the cloud and show them on the ui.
Additional Notes
It should be reminded that Cloud DB is still in beta phase but works pretty well.
For upsert requests, authentication is mandatory. If authentication is not done, the result of upsert will return false. Huawei offers Account Kit and Auth Service for easy authentication
In this article we talked about how can we use Kotlin Flows with Huawei Cloud DB

Does Cloud DB supports BLOB type ?

Related

Developing a Function for Precisely Pushing Ads to Nearby People Using HUAWEI Nearby

More articles like this, you can visit HUAWEI Developer Forum and Medium.​
When you want to find a restaurant and your phone receives a push message recommending nearby restaurants to you at the right moment, will you tap open the message? When you're still hesitating on whether to buy a pair of sneakers in a store and an app sends you a coupon offering you a 50% discount, do you still find the ad bothering?
Ads pushed at the right moment meet users' requirements and won't make users feel bothered. More precise advertising not only reduces unnecessary disturbance to users but also improves user satisfaction with your app. Do you want to build such a function for precisely pushing ads to nearby people?
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Integrate HUAWEI Nearby Service and use the Nearby Beacon Message feature to implement the function. You need to deploy beacons at the place where you want to push messages to nearby people, for example, a mall. Beacons will provide users' locations relative to them so that when a user gets near a restaurant or store, the user will receive a promotion message (such as a coupon or discount information) preconfigured by the merchant in advance. The function demo is as follows.
If you are interested in the implementation details, download the source code from GitHub: https://github.com/HMS-Core/hms-nearby-demo/tree/master/NearbyCanteens
1. Getting Started
If you are already a Huawei developer, skip this step. If you are new to Huawei Mobile Services (HMS), you need to configure app information in AppGallery Connect, enable Nearby Service on the HUAWEI Developers console, and integrate the HMS Core SDK. For details, please refer to the documentation.
1.1 Adding Huawei Maven Repository and AppGallery Connect Plug-in Configurations to the Project-Level build.gradle File
Add the following Huawei maven repository and AppGallery Connect plug-in configurations to your project-level build.gradle file directly:
Code:
buildscript {
repositories {
google()
jcenter()
maven { url 'http://developer.huawei.com/repo/' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
classpath 'com.huawei.agconnect:agcp:1.2.1.301'
}
}
allprojects {
repositories {
google()
jcenter()
maven { url 'http://developer.huawei.com/repo/' }
}
}
1.2 Adding SDK Dependencies to the App-Level build.gradle File
Import the Nearby Service SDKs. The most important SDKs are those starting with com.huawei.
Code:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation "com.huawei.hmf:tasks:1.3.1.301"
implementation "com.huawei.hms:network-grs:1.0.9.302"
implementation 'com.huawei.agconnect:agconnect-core:1.2.1.301'
implementation 'com.huawei.hms:nearby:4.0.4.300'
api 'com.google.code.gson:gson:2.8.5'
}
1.3 Applying for the Network, Bluetooth, and Location Permissions in the AndroidManifest.xml File
The following permissions are required. Each permission name can directly express its purpose. For example, android.permission.INTERNET indicates the network permission, and android.permission.BLUETOOTH indicates the Bluetooth permission.
Code:
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
2. Code Development
2.1 Initialization and Dynamic Permission Application
The onCreate method is called when the current activity is created. In this method, you can perform some preparations, such as applying for necessary permissions and checking whether the Internet connection, Bluetooth, and GPS are enabled.
Code:
@RequiresApi(api = Build.VERSION_CODES.P)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "onCreate");
setContentView(R.layout.activity_canteen);
boolean isSuccess = requestPermissions(this, this);
if (!isSuccess) {
return;
}
Log.i(TAG, "requestPermissions success");
if (!NetCheckUtil.isNetworkAvailable(this)) {
showWarnDialog(Constant.NETWORK_ERROR);
return;
}
if (!BluetoothCheckUtil.isBlueEnabled()) {
showWarnDialog(Constant.BLUETOOTH_ERROR);
return;
}
if (!GpsCheckUtil.isGpsEnabled(this)) {
showWarnDialog(Constant.GPS_ERROR);
return;
}
intView();
init();
}
Register a listener and display a message if Bluetooth, GPS, or network disconnection is detected.
The following uses the AlertDialog component of Android as an example:
Code:
private void showWarnDialog(String content) {
DialogInterface.OnClickListener onClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
android.os.Process.killProcess(android.os.Process.myPid());
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.warn);
builder.setIcon(R.mipmap.warn);
builder.setMessage(content);
builder.setNegativeButton(getText(R.string.btn_confirm), onClickListener);
builder.show();
}
2.2 Beacon Message Reception
The following startScanning method is called in the onStart method to start Bluetooth scanning. In the MessageHandler object, four callback methods are encapsulated: 1. onFound indicates that a beacon message is discovered; 2. OnLost indicates that the message is no longer discoverable; 3. onDistanceChanged indicates the change of the distance between the beacon and the device; 4. onBleSignalChanged indicates that a beacon signal change is detected.
Code:
private void startScanning() {
Log.i(TAG, "startScanning");
mMessageHandler =
new MessageHandler() {
@Override
public void onFound(Message message) {
super.onFound(message);
doOnFound(message);
}
@Override
public void onLost(Message message) {
super.onLost(message);
doOnLost(message);
}
@Override
public void onDistanceChanged(Message message, Distance distance) {
super.onDistanceChanged(message, distance);
}
@Override
public void onBleSignalChanged(Message message, BleSignal bleSignal) {
super.onBleSignalChanged(message, bleSignal);
}
};
MessagePicker msgPicker = new MessagePicker.Builder().includeAllTypes().build();
Policy policy = new Policy.Builder().setTtlSeconds(Policy.POLICY_TTL_SECONDS_INFINITE).build();
GetOption getOption = new GetOption.Builder().setPicker(msgPicker).setPolicy(policy).build();
Task<Void> task = messageEngine.get(mMessageHandler, getOption);
task.addOnFailureListener(
new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, "Login failed:", e);
if (e instanceof ApiException) {
ApiException apiException = (ApiException) e;
int errorStatusCode = apiException.getStatusCode();
if (errorStatusCode == StatusCode.STATUS_MESSAGE_AUTH_FAILED) {
Toast.makeText(mContext, R.string.configuration_error, Toast.LENGTH_SHORT).show();
} else if (errorStatusCode == StatusCode.STATUS_MESSAGE_APP_UNREGISTERED) {
Toast.makeText(mContext, R.string.permission_error, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(mContext, R.string.start_get_beacon_message_failed, Toast.LENGTH_SHORT)
.show();
}
} else {
Toast.makeText(mContext, R.string.start_get_beacon_message_failed, Toast.LENGTH_SHORT)
.show();
}
}
});
}
Personalize the display of a detected beacon message.
The most important method during the process is the doOnFound method, which specifies the personalized display processing mode when the client receives a beacon message.
Code:
private void doOnFound(Message message) {
if (message == null) {
return;
}
String type = message.getType();
if (type == null) {
return;
}
String messageContent = new String(message.getContent());
Log.d(TAG, "New Message:" + messageContent + " type:" + type);
if (type.equalsIgnoreCase(Constant.CANTEEN)) {
operateOnFoundCanteen(messageContent);
} else if (type.equalsIgnoreCase(Constant.NOTICE)) {
operateOnFoundNotice(messageContent);
}
}
Display the personalized message.
The following code only demonstrates one of the message processing modes, including implementing the banner notification and text display effect.
Code:
private void operateOnFoundCanteen(String messageContent) {
CanteenAdapterInfo canteenAdapterInfo =
(CanteenAdapterInfo) JsonUtils.json2Object(messageContent, CanteenAdapterInfo.class);
if (canteenAdapterInfo == null) {
return;
}
String canteenName = canteenAdapterInfo.getCanteenName();
if (canteenName == null) {
return;
}
Log.d(TAG, "canteenName:" + canteenName);
if (!canteenNameList.contains(canteenName)) {
return;
}
String notice = "";
if (receivedNoticeMap.containsKey(canteenName)) {
notice = receivedNoticeMap.get(canteenName);
}
int canteenImage = getCanteenImage(canteenName);
int requestCode = getRequestCode(canteenName);
canteenAdapterInfo.setNotice(notice);
canteenAdapterInfo.setCanteenImage(canteenImage);
canteenAdapterInfo.setShowNotice(true);
canteenAdapterInfo.setRequestCode(requestCode);
canteenAdapterInfoMap.put(canteenName, canteenAdapterInfo);
canteenAdapterInfoList.add(canteenAdapterInfo);
sendNotification(Constant.NOTIFICATION_TITLE, Constant.NOTIFICATION_SUBTITLE, canteenName, requestCode);
runOnUiThread(
new Runnable() {
@Override
public void run() {
searchTipTv.setText(R.string.found_tip);
loadingLayout.setVisibility(View.GONE);
canteenAdapter.setDatas(canteenAdapterInfoList);
}
});
}
Conclusion
This demo uses Bluetooth beacon message subscription function of HUAWEI Nearby Service.
Based on the Nearby Beacon Message capability, you not only can develop an ad push function, but also can implement the following functions:
1. A car or lifestyle app can integrate the capability to identify whether a user is near their car to determine whether to enable keyless access and record the driving track of the car.
2. A business app can integrate the capability to accurately record the locations where employees clock in.
3. A travel or exhibition app can integrate the capability to introduce an exhibit to a user when the user gets near the exhibit.
4. A game app can integrate the capability to make your game interact with the real world, for example, unlocking a game level through a physical object and sending rewards to players who are participating in offline events.
If you are interested and want to learn more, check our development guide at the HUAWEI Developers official website.
Any questions about the process, you can visit HUAWEI Developer Forum.
Hi,
Can we use HMS nearby service to create a chat application and can also provide ads about restaurant when user enter the restaurant in the chat application as a message.
Thanks.
sanghati said:
Hi,
Can we use HMS nearby service to create a chat application and can also provide ads about restaurant when user enter the restaurant in the chat application as a message.
Thanks.
Click to expand...
Click to collapse
Nearby has two businesses, one is Connection, you can use Connection to develop chat functions. The other is Message. Deploy a Beacon in the hotel. After registering the Beacon, set the Beacon message. When the user walks into the restaurant, they can receive the message you set in advance. You can also set some conditions on the cloud service, such as the time period of the user, how long it will be for users in the restaurant, etc. After these conditions are met, you will receive the message. The advantages of this method are: 1. You can modify the rules in real time, 2. More accurate advertising push to avoid unnecessary interruptions.

Integrating Huawei Crash, Push Kit and Account Kit Flutter Plugins to Forks & Spoons: A Restaurant App — Part 1

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.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
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.
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​
> If you are going to implement this service into your app don’t forget to add an app to AppGallery Connect, as described in this document.
> You can find the integration and configuration details of Huawei Flutter Account Kit Plugin here.
Click to expand...
Click to collapse
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.
Code:
/// 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:
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.
Code:
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.
Code:
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.
> You can find the integration and configuration of AppGallery Connect Crash here.
Click to expand...
Click to collapse
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.
Note that below configuration is for Flutter version 1.17 and above. If you are using a lower version please check this document from Huawei Developers.
Click to expand...
Click to collapse
Code:
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​
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.
Code:
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.
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.
Here is the exception report; we can check the stack trace, device information and more in this page, isn’t it cool ?
We can also test an exception by just throwing one and sending its report on the catch clause.
Code:
/// 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.
Code:
// 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.
Code:
@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,
),
);
}
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.
> You can find the integration and configuration details of Huawei Flutter Push Kit Plugin in this document.
Click to expand...
Click to collapse
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.
Code:
void initPush() async {
if (!mounted) return;
// Subscribe to the streams.
Push.getTokenStream.listen(_onTokenEvent, onError: _onTokenError);
// Get the push token.
Push.getToken("");
}
Code:
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.
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.
Let’s quickly prepare a push notification for our belowed users. You can see the message preview on the right side.
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.
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.
XML:
<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.
Code:
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.
Let’s press the button on the notification and see the deep linking in action.
​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.
Code:
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.
Code:
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.
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.
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.
XML:
<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.
Code:
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.
}
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
​
Can we get user Email id and phone number from account kit
useful writeup
lokeshsuryan said:
Can we get user Email id and phone number from account kit
Click to expand...
Click to collapse
You can get the user email but not the phone number.
Does it records Exceptions also in crash services?
shikkerimath said:
Does it records Exceptions also in crash services?
Click to expand...
Click to collapse
Yes you can check the recorded exceptions from the AppGallery Connect as in the article .
turkayavci said:
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.
Code:
/// 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.
Code:
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.
Code:
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.
Code:
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.
Code:
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.
Code:
/// 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.
Code:
// 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.
Code:
@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.
Code:
void initPush() async {
if (!mounted) return;
// Subscribe to the streams.
Push.getTokenStream.listen(_onTokenEvent, onError: _onTokenError);
// Get the push token.
Push.getToken("");
}
Code:
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.
XML:
<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.
Code:
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.
Code:
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.
Code:
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.
XML:
<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.
Code:
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
​
Click to expand...
Click to collapse
Can we sort crash by date in the console?
Basavaraj.navi said:
Can we sort crash by date in the console?
Click to expand...
Click to collapse
For sure.

Build a Game That Features Local In-App Purchases

{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Several months ago, Sony rolled out their all-new PlayStation Plus service, which is home to a wealth of popular classic games. Its official blog wrote that its games catalog "will continue to refresh and evolve over time, so there is always something new to play."
I was totally on board with the idea and so… I thought why not build a lightweight mobile game together with my friends and launch it on a niche app store as a pilot. I did just this. The multiplayer survival game draws on a dark cartoon style and users need to utilize their strategic skills to survive. The game launch was all about sharing ideas, among English users specifically, but it attracted many players from non-English speaking countries like China and Germany. What a surprise!
Like many other game developers, I tried to achieve monetization through in-app user purchases. The app offers many in-game props, such as fancy clothes and accessories, weapons, and skill cards, to deliver a more immersive experience or to help users survive. This posed a significant challenge — as users are based in a number of different countries or regions, the app needs to show product information in the language of the country or region where the user's account is located, as well as the currency. How to do this?
Below is a walkthrough of how I implemented the language and currency localization function and the product purchase function for my app. I turned to HMS Core In-App Purchases (IAP) because it is very accessible. I hope this will help you.
Development Procedure​Product Management​Creating In-App Products​I signed in to AppGallery Connect to enable the IAP service and set relevant parameters first. After configuring the key event notification recipient address for the service, I could create products by selecting my app and going to Operate > Products > Product Management.
IAP supports three types of products, that is, consumables, non-consumables, and subscriptions. For consumables that are depleted as they are used and are repurchasable, I created products including in-game currencies (coins or gems) and items (clothes and accessories). For non-consumables that are purchased once and will never expire, I created products that unlock special game levels or characters for my app. For subscriptions, I went with products such as a monthly game membership to charge users on a recurring basis until they decide to cancel them.
Aside from selecting the product type, I also needed to set the product ID, name, language, and price, and fill in the product description. Voilà. That's how I created the in-app products.
Global Adaptation of Product Information​Here's a good thing about IAP: developers don't need to manage multiple app versions for users from different countries or regions!
All I have to do is complete the multilingual settings of the products in AppGallery Connect. First, select the product languages based on the countries/regions the product is available in. Let's say English and Chinese, in this case. Then, fill in the product information in these two languages. The effect is roughly like this:
LanguageEnglishChineseProduct nameStealth skill card隐身技能卡Product descriptionHelps a user to be invisible so that they can outsurvive their enemies.帮助用户在紧急情况下隐身,打败敌人。
Now it's time to set the product price. I only need to set the price for one country/region and then IAP will automatically adjust the local price based on the exchange rate.
After the price is set, go to the product list page and click Activate. And that's it. The product has been adapted to different locations.
Purchase Implementation​Checking Support for IAP​Before using this kit, send an isEnvReady request to HMS Core (APK) to check whether my HUAWEI ID is located in the country/region where IAP is available. According to the kit's development documentation:
If the request result is successful, my app will obtain an IsEnvReadyResult instance, indicating that the kit is supported in my location.
If the request fails, an exception object will be returned. When the object is IapApiException, use its getStatusCode method to obtain the result code of the request. If the result code is OrderStatusCode.ORDER_HWID_NOT_LOGIN (no HUAWEI ID signed in), use the getStatus method of the IapApiException object to obtain a Status object, and use the startResolutionForResult method of Status to bring up the sign-in screen. Then, obtain the result in the onActivityResult method of Activity. Parse returnCode from the intent returned by onActivityResult. If the value of returnCode is OrderStatusCode.ORDER_STATE_SUCCESS, the country/region where the currently signed-in ID is located supports IAP. Otherwise, an exception occurs.
You guys can see my coding below.
Code:
// Obtain the Activity object.
final Activity activity = getActivity();
Task<IsEnvReadyResult> task = Iap.getIapClient(activity).isEnvReady();
task.addOnSuccessListener(new OnSuccessListener<IsEnvReadyResult>() {
@Override
public void onSuccess(IsEnvReadyResult result) {
// Obtain the execution result.
String carrierId = result.getCarrierId();
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
if (status.getStatusCode() == OrderStatusCode.ORDER_HWID_NOT_LOGIN) {
// HUAWEI ID is not signed in.
if (status.hasResolution()) {
try {
// 6666 is a constant.
// Open the sign-in screen returned.
status.startResolutionForResult(activity, 6666);
} catch (IntentSender.SendIntentException exp) {
}
}
} else if (status.getStatusCode() == OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED) {
// The current country/region does not support IAP.
}
} else {
// Other external errors.
}
}
});
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 6666) {
if (data != null) {
// Call the parseRespCodeFromIntent method to obtain the result.
int returnCode = IapClientHelper.parseRespCodeFromIntent(data);
// Use the parseCarrierIdFromIntent method to obtain the carrier ID returned by the API.
String carrierId = IapClientHelper.parseCarrierIdFromIntent(data);
}
}
}
Showing Products​To show products configured to users, call the obtainProductInfo API in the app to obtain product details.
1. Construct a ProductInfoReq object, send an obtainProductInfo request, and set callback listeners OnSuccessListener and OnFailureListener to receive the request result. Pass the product ID that has been defined and taken effect to the ProductInfoReq object, and specify priceType for a product.
2. If the request is successful, a ProductInfoResult object will be returned. Using the getProductInfoList method of this object, my app can obtain the list of ProductInfo objects. The list contains details of each product, including its price, name, and description, allowing users to see the info of the products that are available for purchase.
Code:
List<String> productIdList = new ArrayList<>();
// Only those products already configured can be queried.
productIdList.add("ConsumeProduct1001");
ProductInfoReq req = new ProductInfoReq();
// priceType: 0: consumable; 1: non-consumable; 2: subscription
req.setPriceType(0);
req.setProductIds(productIdList);
// Obtain the Activity object.
final Activity activity = getActivity();
// Call the obtainProductInfo API to obtain the details of the configured product.
Task<ProductInfoResult> task = Iap.getIapClient(activity).obtainProductInfo(req);
task.addOnSuccessListener(new OnSuccessListener<ProductInfoResult>() {
@Override
public void onSuccess(ProductInfoResult result) {
// Obtain the product details returned upon a successful API call.
List<ProductInfo> productList = result.getProductInfoList();
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
}
});
Initiating a Purchase​The app can send a purchase request by calling the createPurchaseIntent API.
1. Construct a PurchaseIntentReq object to send a createPurchaseIntent request. Pass the product ID that has been defined and taken effect to the PurchaseIntentReq object. If the request is successful, the app will receive a PurchaseIntentResult object, and its getStatus method will return a Status object. The app will display the checkout screen of IAP using the startResolutionForResult method of the Status object.
Code:
// Construct a PurchaseIntentReq object.
PurchaseIntentReq req = new PurchaseIntentReq();
// Only the products already configured can be purchased through the createPurchaseIntent API.
req.setProductId("CProduct1");
// priceType: 0: consumable; 1: non-consumable; 2: subscription
req.setPriceType(0);
req.setDeveloperPayload("test");
// Obtain the Activity object.
final Activity activity = getActivity();
// Call the createPurchaseIntent API to create a product order.
Task<PurchaseIntentResult> task = Iap.getIapClient(activity).createPurchaseIntent(req);
task.addOnSuccessListener(new OnSuccessListener<PurchaseIntentResult>() {
@Override
public void onSuccess(PurchaseIntentResult result) {
// Obtain the order creation result.
Status status = result.getStatus();
if (status.hasResolution()) {
try {
// 6666 is a constant.
// Open the checkout screen returned.
status.startResolutionForResult(activity, 6666);
} catch (IntentSender.SendIntentException exp) {
}
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
}
});
2. After the app opens the checkout screen and the user completes the payment process (that is, successfully purchases a product or cancels the purchase), IAP will return the payment result to your app through onActivityResult. You can use the parsePurchaseResultInfoFromIntent method to obtain the PurchaseResultInfo object that contains the result information.
If the purchase is successful, obtain the purchase data InAppPurchaseData and its signature data from the PurchaseResultInfo object. Use the public key allocated by AppGallery Connect to verify the signature.
When a user purchases a consumable, if any of the following payment exceptions is returned, check whether the consumable was delivered.
Payment failure (OrderStatusCode.ORDER_STATE_FAILED).
A user has purchased the product (OrderStatusCode.ORDER_PRODUCT_OWNED).
The default code is returned (OrderStatusCode.ORDER_STATE_DEFAULT_CODE), as no specific code is available.
Code:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 6666) {
if (data == null) {
Log.e("onActivityResult", "data is null");
return;
}
// Call the parsePurchaseResultInfoFromIntent method to parse the payment result.
PurchaseResultInfo purchaseResultInfo = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data);
switch(purchaseResultInfo.getReturnCode()) {
case OrderStatusCode.ORDER_STATE_CANCEL:
// The user cancels the purchase.
break;
case OrderStatusCode.ORDER_STATE_FAILED:
case OrderStatusCode.ORDER_PRODUCT_OWNED:
// Check whether the delivery is successful.
break;
case OrderStatusCode.ORDER_STATE_SUCCESS:
// The payment is successful.
String inAppPurchaseData = purchaseResultInfo.getInAppPurchaseData();
String inAppPurchaseDataSignature = purchaseResultInfo.getInAppDataSignature();
// Verify the signature using your app's IAP public key.
// Start delivery if the verification is successful.
// Call the consumeOwnedPurchase API to consume the product after delivery if the product is a consumable.
break;
default:
break;
}
}
}
Confirming a Purchase​After a user pays for a purchase or subscription, the app checks whether the payment is successful based on the purchaseState field in InAppPurchaseData. If purchaseState is 0 (already paid), the app will deliver the purchased product or service to the user, then send a delivery confirmation request to IAP.
For a consumable, parse purchaseToken from InAppPurchaseData in JSON format to check the delivery status of the consumable.
After the consumable is successfully delivered and its purchaseToken is obtained, your app needs to use the consumeOwnedPurchase API to consume the product and instruct the IAP server to update the delivery status of the consumable. purchaseToken is passed in the API call request. If the consumption is successful, the IAP server will reset the product status to available for purchase. Then the user can buy it again.
Code:
// Construct a ConsumeOwnedPurchaseReq object.
ConsumeOwnedPurchaseReq req = new ConsumeOwnedPurchaseReq();
String purchaseToken = "";
try {
// Obtain purchaseToken from InAppPurchaseData.
InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseData);
purchaseToken = inAppPurchaseDataBean.getPurchaseToken();
} catch (JSONException e) {
}
req.setPurchaseToken(purchaseToken);
// Obtain the Activity object.
final Activity activity = getActivity();
// Call the consumeOwnedPurchase API to consume the product after delivery if the product is a consumable.
Task<ConsumeOwnedPurchaseResult> task = Iap.getIapClient(activity).consumeOwnedPurchase(req);
task.addOnSuccessListener(new OnSuccessListener<ConsumeOwnedPurchaseResult>() {
@Override
public void onSuccess(ConsumeOwnedPurchaseResult result) {
// Obtain the execution result.
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
}
});
For a non-consumable, the IAP server returns the confirmed purchase data by default. After the purchase is successful, the user does not need to confirm the transaction, and the app delivers the product.
For a subscription, no acknowledgment is needed after a successful purchase. However, as long as the user is entitled to the subscription (that is, the value of InApppurchaseData.subIsvalid is true), the app should offer services.
Conclusion​It's a great feeling to make a game, and it's an even greater feeling when that game makes you money.
In this article, I shared my experience of building an in-app purchase function for my mobile survival game. To make it more suitable for a global market, I used some gimmicks from HMS Core In-App Purchases to configure product information in the language of the country or region where the user's account is located. In short, this streamlines the purchase journey for users wherever they are located.
Did I miss anything? I'm looking forward to hearing your ideas.

Build a Seamless Sign-in Experience Across Different Apps and Platforms with Keyring

Mobile apps have significantly changed the way we live, bringing about greater convenience. With our mobiles we can easily book hotels online when we go sightseeing, buy train and flight tickets online for business trips, or just pay for a dinner using scan and pay.
There is rarely a one-app-fits-all approach of offering such services, so users have to switch back and forth between multiple apps. This also requires users to register and sign in to different apps, which is a trouble itself because users will need to complete complex registration process and repeatedly enter their account names and passwords.
In addition, as technology develops, a developer usually has multiple Android apps and app versions, such as the quick app and web app, for different platforms. If users have to repeatedly sign in to different apps or versions by the same developer, the churn rate will likely increase. What's more, the developer may need to even pay for sending SMS messages if users choose to sign in to their apps through SMS verification codes.
Is there anything the developer can do to streamline the sign-in process between different apps and platforms so that users do not need to enter their account names and passwords again and again?
Well fortunately, HMS Core Keyring makes this possible. Keyring is a Huawei service that offers credential management APIs for storing user credentials locally on users' Android phones and tablets and sharing the credentials between different apps and different platform versions of an app. Developers can call relevant APIs in their Android apps, web apps, or quick apps to use Keyring services, such as encrypt the sign-in credentials of users for local storage on user devices and share the credentials between different apps and platforms, thus creating a seamless sign-in experience for users across different apps and platforms. Besides, all credentials will be stored in Keyring regardless of which type of APIs developers are calling, to implement unified credential management and sharing.
In this article, I'll share how I used Keyring to manage and share sign-in credentials of users. I hope this will help you.
Advantages​
First, I'd like to explain some advantages of Keyring.
Building a seamless sign-in experience
Your app can call Keyring APIs to obtain sign-in credentials stored on user devices, for easy sign-in.
Ensuring data security and reliability
Keyring encrypts sign-in credentials of users for local storage on user devices and synchronizes the credentials between devices via end-to-end encryption technology. The encrypted credentials cannot be decrypted on the cloud.
Reducing the churn rate during sign-in
Keyring can simplify the sign-in process for your apps, thus reducing the user churn rate.
Reducing the operations cost
With Keyring, you can reduce the operations cost, such as the expense for SMS messages used by users to sign in to your app.
Development Procedure​
Next, let's look at how to integrate Keyring. Before getting started, you will need to make some preparations, such as register as a Huawei developer, generate and configure your signing certificate fingerprint in AppGallery Connect, and enable Keyring. You can click here to learn about the detailed preparation steps, which will not be introduced in this article.
After making necessary preparations, you can now start integrating the Keyring SDK. I'll detail the implementation steps in two scenarios.
User Sign-in Scenario​
In this scenario, you need to follow the steps below to implement relevant logic.
1. Initialize the CredentialClient object in the onCreate method of your activity. Below is a code snippet example.
Code:
CredentialClient credentialClient = CredentialManager.getCredentialClient(this);
2. Check whether a credential is available. Below is a code snippet example.
Code:
List<AppIdentity> trustedAppList = new ArrayList<>();
trustedAppList.add(new AndroidAppIdentity("yourAppName", "yourAppPackageName", "yourAppCodeSigningCertHash"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "www.yourdomain.com"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "login.yourdomain.com"));
SharedCredentialFilter sharedCredentialFilter = SharedCredentialFilter.acceptTrustedApps(trustedAppList);
credentialClient.findCredential(sharedCredentialFilter, new CredentialCallback<List<Credential>>() {
@Override
public void onSuccess(List<Credential> credentials) {
if (credentials.isEmpty()) {
Toast.makeText(MainActivity.this, R.string.no_available_credential, Toast.LENGTH_SHORT).show();
} else {
for (Credential credential : credentials) {
}
}
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this, R.string.query_credential_failed, Toast.LENGTH_SHORT).show();
}
});
3. Call the Credential.getContent method to obtain the credential content and obtain the result from CredentialCallback<T>. Below is a code snippet example.
Code:
private Credential mCredential;
// Obtained credential.
mCredential.getContent(new CredentialCallback<byte[]>() {
@Override
public void onSuccess(byte[] bytes) {
String hint = String.format(getResources().getString(R.string.get_password_ok),
new String(bytes));
Toast.makeText(MainActivity.this, hint, Toast.LENGTH_SHORT).show();
mResult.setText(new String(bytes));
}
@Override
public void onFailure(long l, CharSequence charSequence) {
Toast.makeText(MainActivity.this, R.string.get_password_failed,
Toast.LENGTH_SHORT).show();
mResult.setText(R.string.get_password_failed);
}
});
4. Call the credential saving API when a user enters a new credential, to save the credential. Below is a code snippet example.
Code:
AndroidAppIdentity app2 = new AndroidAppIdentity(sharedToAppName,
sharedToAppPackage, sharedToAppCertHash);
List<AppIdentity> sharedAppList = new ArrayList<>();
sharedAppList.add(app2);
Credential credential = new Credential(username, CredentialType.PASSWORD, userAuth,
password.getBytes());
credential.setDisplayName("user_niceday");
credential.setSharedWith(sharedAppList);
credential.setSyncable(true);
credentialClient.saveCredential(credential, new CredentialCallback<Void>() {
@Override
public void onSuccess(Void unused) {
Toast.makeText(MainActivity.this,
R.string.save_credential_ok,
Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this,
R.string.save_credential_failed + " " + errorCode + ":" + description,
Toast.LENGTH_SHORT).show();
}
});
User Sign-out Scenario​
Similarly, follow the steps below to implement relevant logic.
1. Initialize the CredentialClient object in the onCreate method of your activity. Below is a code snippet example.
Code:
CredentialClient credentialClient = CredentialManager.getCredentialClient(this);
2. Check whether a credential is available. Below is a code snippet example.
Code:
List<AppIdentity> trustedAppList = new ArrayList<>();
trustedAppList.add(new AndroidAppIdentity("yourAppName", "yourAppPackageName", "yourAppCodeSigningCertHash"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "www.yourdomain.com"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "login.yourdomain.com"));
SharedCredentialFilter sharedCredentialFilter = SharedCredentialFilter.acceptTrustedApps(trustedAppList);
credentialClient.findCredential(sharedCredentialFilter, new CredentialCallback<List<Credential>>() {
@Override
public void onSuccess(List<Credential> credentials) {
if (credentials.isEmpty()) {
Toast.makeText(MainActivity.this, R.string.no_available_credential, Toast.LENGTH_SHORT).show();
} else {
for (Credential credential : credentials) {
// Further process the available credentials, including obtaining the credential information and content and deleting the credentials.
}
}
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this, R.string.query_credential_failed, Toast.LENGTH_SHORT).show();
}
});
3. Call the deleteCredential method to delete the credential and obtain the result from CredentialCallback. Below is a code snippet example.
Code:
credentialClient.deleteCredential(credential, new CredentialCallback<Void>() {
@Override
public void onSuccess(Void unused) {
String hint = String.format(getResources().getString(R.string.delete_ok),
credential.getUsername());
Toast.makeText(MainActivity.this, hint, Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(long errorCode, CharSequence description) {
String hint = String.format(getResources().getString(R.string.delete_failed),
description);
Toast.makeText(MainActivity.this, hint, Toast.LENGTH_SHORT).show();
}
});
Keyring offers two modes for sharing credentials: sharing credentials using API parameters and sharing credentials using Digital Asset Links. I will detail the two modes below.
Sharing Credentials Using API Parameters​
In this mode, when calling the saveCredential method to save credentials, you can call the setSharedWith method to set parameters of the Credential object, to implement credential sharing. A credential can be shared to a maximum of 128 apps.
The sample code is as follows:
Code:
AndroidAppIdentity app1 = new AndroidAppIdentity("your android app name",
"your android app package name", "3C:99:C3:....");
QuickAppIdentity app2 = new QuickAppIdentity("your quick app name",
"your quick app package name", "DC:99:C4:....");
List<AppIdentity> sharedAppList = new ArrayList<>(); // List of apps with the credential is shared.
sharedAppList.add(app1);
sharedAppList.add(app2);
Credential credential = new Credential("username", CredentialType.PASSWORD, true,
"password".getBytes());
credential.setSharedWith(sharedAppList); // Set the credential sharing relationship.
credentialClient.saveCredential(credential, new CredentialCallback<Void>() {
@Override
public void onSuccess(Void unused) {
Toast.makeText(MainActivity.this,
R.string.save_credential_ok,
Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this,
R.string.save_credential_failed + " " + errorCode + ":" + description,
Toast.LENGTH_SHORT).show();
}
});
Sharing Credentials Using Digital Asset Links​
In this mode, you can add credential sharing relationships in the AndroidManifest.xml file of your Android app. The procedure is as follows:
1. Add the following content to the <application> element in the AndroidManifest.xml file:
Code:
<application>
<meta-data
android:name="asset_statements"
android:value="@string/asset_statements" />
</application>
2. Add the following content to the res\values\strings.xml file:
Code:
<string name="asset_statements">your digital asset links statements</string>
The Digital Asset Links statements are JSON strings comply with the Digital Asset Links protocol. The sample code is as follows:
Code:
[{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "web",
"site": "https://developer.huawei.com" // Set your website domain name.
}
},
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "your android app package name",
"sha256_cert_fingerprints": [
"F2:52:4D:..."
]
}
},
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "quick_app",
"package_name": "your quick app package name",
"sha256_cert_fingerprints": [
"C3:68:9F:..."
]
}
}
]
The relation attribute has a fixed value of ["delegate_permission/common.get_login_creds"], indicating that the credential is shared with apps described in the target attribute.
And that's all for integrating Keyring. That was pretty straightforward, right? You can click here to find out more about Keyring and try it out.
Conclusion​
More and more developers are prioritizing the need for a seamless sign-in experience to retain users and reduce the user churn rate. This is especially true for developers with multiple apps and app versions for different platforms, because it can help them share the user base of their different apps. There are many ways to achieve this. As I illustrated earlier in this article, my solution for doing so is to integrate Keyring, which turns out to be very effective. If you have similar demands, have a try at this service and you may be surprised.
Did I miss anything? Let me know in the comments section below.

A Guide for Integrating HMS Core Push Kit into a HarmonyOS App

With the proliferation of mobile Internet, push messaging has become a very effective way for mobile apps to achieve business success because it improves user engagement and stickiness by allowing developers to send messages to a wide range of users in a wide range of scenarios, such as when taking the subway or bus, having a meal in a restaurant, chatting with friends, and many more. No matter what the scenario is, a push message is always a great way for you to directly "talk" to your users, and for your users to obtain useful information.
The messaging method, however, may vary depending on the mobile device operating system, such as HarmonyOS, Android, and iOS. For this article, we'll be focusing on HarmonyOS. Is there a product or service that can be used to push messages to HarmonyOS apps effectively?
The answer, of course, is yes. After a little bit of research, I decided that HMS Core Push Kit for HarmonyOS (Java) is the best solution for me. This kit empowers HarmonyOS apps to send notification and data messages to mobile phones and tablets based on push tokens. A maximum of 1000 push tokens can be entered at a time to send messages.
Data messages are processed by apps on user devices. After a device receives a message containing data or instructions from the Push Kit server, the device passes the message to the target app instead of directly displaying it. The app then parses the message and triggers the required action (for example, going to a web page or an in-app page). Data messages are generally used in scenarios such as VoIP calls, voice broadcasts, and when interacting with friends. You can also customize the display style of such messages to improve their efficacy. Note that the data message delivery rate for your app may be affected by system restrictions and whether your app is running in the background.
In the next part of this article, I'll demonstrate how to use the kit's abilities to send messages. Let's begin with implementation.
Development Preparations​
You can click here to learn about how to prepare for the development. I won't be going into the details in this article.
App Development​
Obtaining a Push Token​
A push token uniquely identifies your app on a device. Your app calls the getToken method to obtain a push token from the Push Kit server. Then you can send messages to the app based on the obtained push token. If no push token is returned by getToken, you can use the onNewToken method to obtain one.
You are advised to upload push tokens to your app server as a list and update the list periodically. With the push token list, you can call the downlink message sending API of the Push Kit server to send messages to users in batches.
The detailed procedure is as follows:
1. Create a thread and call the getToken method to obtain a push token. (It is recommended that the getToken method be called in the first Ability after app startup.)
Code:
public class TokenAbilitySlice extends AbilitySlice {
private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD001234,
"TokenAbilitySlice");
private void getToken() {
// Create a thread.
new Thread("getToken") {
@Override
public void run() {
try {
// Obtain the value of client/app_id from the agconnect-services.json file.
String appId = "your APP_ID";
// Set tokenScope to HCM.
String tokenScope = "HCM";
// Obtain a push token.
String token = HmsInstanceId.getInstance(getAbility().getAbilityPackage(), TokenAbilitySlice.this).getToken(appId, tokenScope);
} catch (ApiException e) {
// An error code is recorded when the push token fails to be obtained.
HiLog.error(LABEL_LOG, "get token failed, the error code is %{public}d", e.getStatusCode());
}
}
}.start();
}
}
2. Override the onNewToken method in your service (extended HmsMessageService). When the push token changes, the new push token can be returned through the onNewToken method.
Code:
public class DemoHmsMessageServiceAbility extends HmsMessageService {
private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD001234, "DemoHmsMessageServiceAbility");
@Override
// Obtain a token.
public void onNewToken(String token) {
HiLog.info(LABEL_LOG, "onNewToken called, token:%{public}s", token);
}
@Override
// Record an error code if the token fails to be obtained.
public void onTokenError(Exception exception) {
HiLog.error(LABEL_LOG, "get onNewtoken error, error code is %{public}d", ((ZBaseException)exception).getErrorCode());
}
}
Obtaining Data Message Content​
Override the onMessageReceived method in your service (extended HmsMessageService). Then you can obtain the content of a data message as long as you send the data message to user devices.
Code:
public class DemoHmsMessageServiceAbility extends HmsMessageService {
private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD001234,
"DemoHmsMessageServiceAbility");
@Override
public void onMessageReceived(ZRemoteMessage message) {
// Print the content field of the data message.
HiLog.info(LABEL_LOG, "get token, %{public}s", message.getToken());
HiLog.info(LABEL_LOG, "get data, %{public}s", message.getData());
ZRemoteMessage.Notification notification = message.getNotification();
if (notification != null) {
HiLog.info(LABEL_LOG, "get title, %{public}s", notification.getTitle());
HiLog.info(LABEL_LOG, "get body, %{public}s", notification.getBody());
}
}
}
Sending Messages​
You can send messages in either of the following ways:
Sign in to AppGallery Connect to send messages. You can click here for details about how to send messages using this method.
Call the Push Kit server API to send messages. Below, I'll explain how to send messages using this method.
1. Call the
https://oauth-login.cloud.huawei.com/oauth2/v3/token
API of the Account Kit server to obtain an access token.
Below is the request sample code:
Code:
POST /oauth2/v3/token HTTP/1.1
Host: oauth-login.cloud.huawei.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<Client ID>&client_secret=<Client secret>
Below is the response sample code:
Code:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
{
"access_token": "<Returned access token>",
"expires_in": 3600,
"token_type": "Bearer"
}
2. Call the Push Kit server API to send messages. Below is the request sample code:
The following is the URL for calling the API using HTTPS POST:
Code:
POST https://push-api.cloud.huawei.com/v1/clientid/messages:send
The request header looks like this:
Code:
Content-Type: application/json; charset=UTF-8
Authorization: Bearer CF3Xl2XV6jMK************************DgAPuzvNm3WccUIaDg==
The request body (of a notification message) looks like this:
Code:
{
"validate_only": false,
"message": {
"android": {
"notification": {
"title": "test title",
"body": "test body",
"click_action": {
"type": 3
}
}
},
"token": ["pushtoken1"]
}
}
Customizing Actions to Be Triggered upon Message Tapping​
You can customize the action triggered when a user taps the message, for example, opening the app home page, a website URL, or a specific page within an app.
Opening the App Home Page​
You can sign in to AppGallery Connect to send messages and specify to open the app home page when users tap the sent messages.
You can also call the Push Kit server API to send messages, as well as carry the click_action field in the message body and set type to 3 (indicating to open the app home page when users tap the sent messages).
Code:
{
"validate_only": false,
"message": {
"android": {
"notification": {
"title": "test title",
"body": "test body",
"click_action": {
"type": 3
}
}
},
"token": ["pushtoken1"]
}
}
Opening a Web Page​
You can sign in to AppGallery Connect to send messages and specify to open a web page when users tap the sent messages.
You can also call the Push Kit server API to send messages, as well as carry the click_action field in the message body and set type to 2 (indicating to open a web page when users tap the sent messages).
Code:
{
"validate_only": false,
"message": {
"android": {
"notification": {
"title": "test title",
"body": "test body",
"click_action": {
"type": 2,
"url":"https://www.huawei.com"
}
}
},
"token": ["pushtoken1"]
}
}
Opening a Specified App Page​
Create a custom page in your app. Taking MyActionAbility as an example, add the skills field of the ability to the config.json file in the entry/src/main directory of your project. In the file, the entities field has a fixed value of entity.system.default, and the value (for example, com.test.myaction) of actions can be changed as needed.
Code:
{
"orientation": "unspecified",
"name": "com.test.java.MyActionAbility",
"icon": "$media:icon",
"description": "$string:myactionability_description",
"label": "$string:entry_MyActionAbility",
"type": "page",
"launchType": "standard",
"skills": [
{
"entities": ["entity.system.default"],
"actions": ["com.test.myaction"]
}
]
}
Sign in to AppGallery Connect to send messages and specify to open the specified app page when users tap the sent messages. (The value of action should be that of actions defined in the previous step.)
You can also call the Push Kit server API to send messages, as well as carry the click_action and action fields in the message body and set type to 1 (indicating to open the specified app page when users tap the sent messages). The value of action should be that of actions defined in the previous step.
Code:
{
"validate_only": false,
"message": {
"android": {
"notification": {
"title": "test title",
"body": "test body",
"click_action": {
"type": 1,
"action":"com.test.myaction"
}
}
},
"token": ["pushtoken1"]
}
}
Transferring Data​
When sending a message, you can carry the data field in the message. When a user taps the message, data in the data field will be transferred to the app in the specified way.
1. Carry the data field in the message to be sent. You can do this in either of the following ways:
Sign in to AppGallery Connect to send the message, as well as carry the data field in the message body and set the key-value pair in the field.
Call the Push Kit server API to send the message and carry the data field in the message body.
Code:
{
"validate_only": false,
"message": {
"android": {
"notification": {
"title": "test title",
"body": "test body",
"click_action": {
"type": 1,
"action":"com.test.myaction"
}
},
"data": "{'key_data':'value_data'}"
},
"token": ["pushtoken1"]
}
}
2. Implement the app page displayed after message tapping to obtain the data field. Here, we assume that the app home page (MainAbilitySlice) is displayed after message tapping.
Code:
public class MainAbilitySlice extends AbilitySlice {
private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD001234, "myDemo");
@Override
public void onStart(Intent intent) {
HiLog.info(LABEL_LOG, "MainAbilitySlice get started...");
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
// Call the parsing method.
parseIntent(intent);
}
private void parseIntent(Intent intent){
if (intent == null){return;}
IntentParams intentParams = intent.getParams();
if (intentParams == null) {return;}
// Obtain the key-value pair in the data field.
String key = "key_data";
Object obj = intentParams.getParam(key);
try{
// Print the key-value pair in the data field.
HiLog.info(LABEL_LOG, "my key: %{public}s, my value: %{public}s", key, obj);
}catch (Exception e){
HiLog.info(LABEL_LOG, "catch exception : " + e.getMessage());
}
}
}
Conclusion​
Today's highly-developed mobile Internet has made push messaging an important and effective way for mobile apps to improve user engagement and stickiness.
In this article, I demonstrated how to use HMS Core Push Kit to send messages to HarmonyOS apps based on push tokens. As demonstrated, the whole implementation process is both straightforward and cost-effective, and results in a better messaging effect for push messages.

Categories

Resources