[Huawei Developers] HMS Community:
Introduction

In this article, we will be integrating Huawei Remote Configuration service, Account Kit, Crash Kit and Huawei IAP Service in Flutter DietApp. Flutter plugin provides simple and convenient way to experience authorization of users. Flutter Account Plugin allows users to connect to the Huawei ecosystem using their Huawei IDs from the different devices such as mobiles phones and tablets, added users can login quickly and conveniently sign in to apps with their Huawei IDs after granting initial access permission.

We will also learn to integrate Huawei Crash Service in this application. It is a responsibility of the developer to build crash free i.e. unexpected exit of application, it is very difficult to find the cause of the crash in huge application code base. This unexpected crash makes app users annoying and may lead to business loss and reduce market value of the product and the company. To avoid such crashes Huawei provides Crash service which makes developer to find the crash and cause of unexpected exit of the application in AG-console.

Crash SDK provides very simple and developer need not code much to implementation crash service. You can also download the detailed crash report whenever required.

Remote Configuration is a service provided by Huawei. It provides cloud-based services. Once you integrate the client SDK, your app can periodically fetch parameter values from the cloud. The service checks whether the parameter that your app tries fetching has an on-cloud value update and returns the new value if so. Based on the parameter values fetched, you can implement service processing logic to change behaviour or appearance of your app. In this sample i.e. DietApp, we are fetching the Personal Exercise data using Remote Configuration service by AppGallery.

Flutter In-App Purchases (IAP) plugin provides communication between the HMS Core IAP SDK and Flutter platform. Huawei IAP allows you to offer in-app purchases and facilitates in-app payment. Users can purchase a variety of virtual products, including one-time virtual products and subscriptions, directly within your app.

Flutter IAP plugin provides the following APIs, which are also core capabilities you need to quickly build apps with which your users can buy, consume, and subscribe to services you provide:

isEnvReady: Returns a response which indicates user's sign-in and location status for HUAWEI IAP.

isSandboxActivated: Returns a response which indicates user's account capabilities for sandbox testing.

obtainProductInfo: Returns a list of products.

startIapActivity: Starts an activity to manage and edit subscriptions.

createPurchaseIntent: Starts an activity to purchase a desired product or subscribe to a product.

consumeOwnedPurchase: Consumes a desired product.

obtainOwnedPurchases: Returns the list of products purchased by a user.

obtainOwnedPurchaseRecord: Returns the list of products purchased and consumed by a user.

Previous article

Please check my previous articles, if you have not gone through, click on part-5.

Development Overview

You need to install Flutter and Dart plugin in IDE and I assume that you have prior knowledge about the Flutter and Dart.

Hardware Requirements

A computer (desktop or laptop) running Windows 10.

Android phone (with the USB cable), which is used for debugging.

Software Requirements

Java JDK 1.7 or later.

Android studio software or Visual Studio or Code installed.

HMS Core (APK) 4.X or later.

Integration process

Step 1: Create Flutter project.

Step 2: Add the App level gradle dependencies. Choose inside project Android > app > build.gradle.

apply plugin: 'com.android.application'

apply plugin: 'com.huawei.agconnect'

Root level gradle dependencies

maven {url 'https://developer.huawei.com/repo/'}
classpath 'com.huawei.agconnect:agcp:1.5.2.300'

Step 3: Add the below permissions in Android Manifest file.

<uses-permission android:name="android.permission.INTERNET" />

Step 4: Download flutter IAP kit plugins

Step 5: Add downloaded file into parent directory of the project. Declare plugin path in pubspec.yaml file under dependencies.

Add path location for asset image.

Let's start coding

SplashScreen.dart

[/B]
class SplashScreen extends StatefulWidget {
@override
_SplashScreenState createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
Timer(
Duration(seconds: 3),
() => Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const LoginScreen())));
}

@override
Widget build(BuildContext context) {
return Container(
color: Colors.white, child: Image.asset('images/logo_huawei.png'));
}
}

[B]

loginScreen.dart

[/B][/B]
class LoginScreen extends StatelessWidget {
const LoginScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: LoginDemo(),
);
}
}

class LoginDemo extends StatefulWidget {
@override
_LoginDemoState createState() => _LoginDemoState();
}

class _LoginDemoState extends State<LoginDemo> {
final HMSAnalytics _hmsAnalytics = new HMSAnalytics();
@override
void initState() {
_enableLog();
super.initState();
}

Future<void> _enableLog() async {
_hmsAnalytics.setUserId("TestUserDietApp");
await _hmsAnalytics.enableLog();
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Login"),
backgroundColor: Colors.blue,
),
body: RefreshIndicator(
onRefresh: showToast,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 90.0),
child: Center(
child: Container(
width: 320,
height: 220,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(50.0)),
child: Image.asset('images/logo_huawei.png')),
),
),
Padding(
padding: EdgeInsets.only(
left: 40.0, right: 40.0, top: 15, bottom: 0),
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email',
hintText: 'Enter valid email id '),
),
),
Padding(
padding: const EdgeInsets.only(
left: 40.0, right: 40.0, top: 10, bottom: 0),
child: TextField(
obscureText: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
hintText: 'Enter password'),
),
),
FlatButton(
onPressed: () {
//TODO FORGOT PASSWORD SCREEN GOES HERE
},
child: Text(
'Forgot Password',
style: TextStyle(color: Colors.blue, fontSize: 15),
),
),
Container(
height: 50,
width: 270,
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(20)),
child: FlatButton(
onPressed: () async {
try {
try {
final bool result = await AccountAuthService.signOut();
if (result) {
final bool response =
await AccountAuthService.cancelAuthorization();
}
} on Exception catch (e) {
print(e.toString());
}
} on Exception catch (e) {
print(e.toString());
}
},
child: GestureDetector(
onTap: () async {
try {
final bool response =
await AccountAuthService.cancelAuthorization();
} on Exception catch (e) {
print(e.toString());
}
},
child: Text(
'Login',
style: TextStyle(color: Colors.white, fontSize: 22),
),
),
),
),
SizedBox(
height: 20,
),
Container(
height: 50,
width: 270,
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(5)),
child: HuaweiIdAuthButton(
theme: AuthButtonTheme.FULL_TITLE,
buttonColor: AuthButtonBackground.RED,
borderRadius: AuthButtonRadius.MEDIUM,
onPressed: () {
signInWithHuaweiID();
}),
),
SizedBox(
height: 30,
),
GestureDetector(
onTap: () {
//showBannerAd();
},
child: Text('New User? Create Account'),
),
],
),
),
),
);
}

void signInWithHuaweiID() async {
AccountAuthParamsHelper helper = new AccountAuthParamsHelper();
String name = '';
helper.setAuthorizationCode();
try {
// The sign-in is successful, and the user's ID information and authorization code are obtained.

Future<AuthAccount> account = AccountAuthService.signIn();
account.then(
(value) => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MyHomePage(
title: value.displayName.toString(),
),
),
),
);
} on Exception catch (e) {
print(e.toString());
}
}

Future<void> showToast() async {
Fluttertoast.showToast(
msg: "Refreshing.. ",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 1,
backgroundColor: Colors.lightBlue,
textColor: Colors.white,
fontSize: 16.0);
}
}

[B][B]

main.dart

[/B][/B][/B]
void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Exercise&DietApp',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SplashScreen(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
late double? age, height, weight, tagetWeight, days;
TextEditingController ageController = TextEditingController();
TextEditingController genderController = TextEditingController();
TextEditingController heightController = TextEditingController();
TextEditingController weightController = TextEditingController();
TextEditingController targetWeightController = TextEditingController();
TextEditingController inDaysWeightController = TextEditingController();
TextEditingController dietPlanController = TextEditingController();
TextEditingController activeLevelPlanController = TextEditingController();
late SharedPreferences prefs;

String _genderLabel = "Male";
String _dietLabel = "Veg";
String _activeLable = "Low-Active";
final _genderList = ["Male", "Women"];
final _dietPlanList = ["Veg", "Non Veg", "Egg"];
final _activeLevelList = ["Low-Active", "Mid-Active", "Very Active"];
String _token = '';

void initState() {
initPreferences();
initTokenStream();
super.initState();
}

Future<void> initTokenStream() async {
if (!mounted) return;
Push.getTokenStream.listen(_onTokenEvent, onError: _onTokenError);

getToken();
}

void getToken() async {
// Call this method to request for a token
Push.getToken("");
}

void _onTokenEvent(String event) {
// Requested tokens can be obtained here
setState(() {
_token = event;
print("TokenEvent: " + _token);
});
}

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

@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text(widget.title.toString()),
automaticallyImplyLeading: false,
),
body: Center(
child: SingleChildScrollView(
reverse: true,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 0.0),
child: Center(
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(60.0)),
child: Image.asset('images/nu_icon.png')),
),
),
Padding(
padding: const EdgeInsets.only(
left: 22.0, right: 22.0, top: 15, bottom: 0),
child: TextField(
controller: ageController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Age',
hintText: 'Enter valid age '),
),
),

Padding(
padding: const EdgeInsets.only(
left: 22.0, right: 22.0, top: 15, bottom: 0),
child: TextField(
controller: heightController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Height',
hintText: 'Enter height '),
),
),
Padding(
padding: const EdgeInsets.only(
left: 22.0, right: 22.0, top: 15, bottom: 0),
child: TextField(
controller: weightController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Weight',
hintText: 'Enter Weight '),
),
),
Padding(
padding: const EdgeInsets.only(
left: 22.0, right: 22.0, top: 15, bottom: 0),
child: TextField(
controller: targetWeightController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Target Weight',
hintText: 'Enter target Weight '),
),
),
Padding(
padding: const EdgeInsets.only(
left: 22.0, right: 22.0, top: 15, bottom: 0),
child: TextField(
controller: inDaysWeightController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'In days',
hintText: 'How quickly you want loose/gain weight '),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Text('Gender :'),
DropdownButton(
items: _genderList
.map((String item) => DropdownMenuItem<String>(
child: SizedBox(
height: 22,
width: 100,
child: Text(item,
style: TextStyle(fontSize: 20))),
value: item))
.toList(),
onChanged: (value) {
setState(() {
_genderLabel = value.toString();
});
},
value: _genderLabel,
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Diet Plan :'),
DropdownButton(
items: _dietPlanList
.map((String item) => DropdownMenuItem<String>(
child: SizedBox(
height: 22,
width: 100,
child: Text(item,
style: TextStyle(fontSize: 20))),
value: item))
.toList(),
onChanged: (value) {
setState(() {
_dietLabel = value.toString();

print('Diet plan changed to $value');
});
},
value: _dietLabel,
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Active Level :'),
DropdownButton(
items: _activeLevelList
.map((String item) => DropdownMenuItem<String>(
child: SizedBox(
height: 22,
width: 100,
child: Text(item,
style: TextStyle(fontSize: 20))),
value: item))
.toList(),
onChanged: (value) {
setState(() {
_activeLable = value.toString();
print('Active level changed to $value');
});
},
value: _activeLable,
)
],
)
,
Padding(
padding: const EdgeInsets.only(
left: 22.0, right: 22.0, top: 20, bottom: 0),
child: MaterialButton(
child: const Text(
'Next',
style: TextStyle(
fontSize: 22,
color: Colors.white,
fontWeight: FontWeight.w300),
),
height: 45,
minWidth: 140,
color: Colors.lightBlue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
onPressed: () {
print('Button clicked.....');
age = double.parse(ageController.text);
height = double.parse(heightController.text);
weight = double.parse(weightController.text);
tagetWeight = double.parse(targetWeightController.text);
days = double.parse(inDaysWeightController.text);

storeDataLocally();
},
),
),
],
),
),
),
),
);
}

Future<void> storeDataLocally() async {
prefs.setString("name", widget.title.toString());
prefs.setDouble("age", age!);
prefs.setDouble("height", height!);
prefs.setString("gender", _genderLabel);
prefs.setDouble("weight", weight!);
prefs.setString("dietPlan", _dietLabel);
prefs.setDouble("targetWeight", tagetWeight!);
prefs.setString("activeLevel", _activeLable);
prefs.setDouble("inDays", days!);

Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DietPlanScreen()),
);
}

Future<void> initPreferences() async {
prefs = await SharedPreferences.getInstance();
showData();
}

void showData() {
print("Gender ===>" + prefs.getString('gender').toString());
print("Diet Plan ===>" + prefs.getString('dietPlan').toString());
print("Active Level ===>" + prefs.getString('activeLevel').toString());
if (prefs.getDouble('age')! > 10) {
ageController.text = prefs.getDouble('age').toString();
heightController.text = prefs.getDouble('height').toString();
targetWeightController.text = prefs.getDouble('targetWeight').toString();
genderController.text = prefs.getString('gender').toString();
dietPlanController.text = prefs.getString('dietPlan').toString();
weightController.text = prefs.getDouble('weight').toString();
inDaysWeightController.text = prefs.getDouble('inDays').toString();
activeLevelPlanController.text =
prefs.getString('activeLevel').toString();
if (prefs.getString('gender').toString() != null &&
prefs.getString('gender').toString() != '') {
_genderLabel = prefs.getString('gender').toString();
}
if (prefs.getString('dietPlan').toString() != null) {
_dietLabel = prefs.getString('dietPlan').toString();
}
if (prefs.getString('activeLevel').toString() != null) {
_activeLable = prefs.getString('activeLevel').toString();
}
}
}
}

[B][B][B]

tabScreen.dart

[/B][/B][/B][/B]
class TabScreen extends StatefulWidget {
@override
TabScreenPage createState() => TabScreenPage();
}

class TabScreenPage extends State<TabScreen> {
final rnd = math.Random();
List<String> gridItems = [];

@override
void initState() {
// TODO: implement initState
fetchRemoteConfiguration();
super.initState();
}

@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text(
'Diet Plan',
),
centerTitle: true,
backgroundColor: Colors.blue,
elevation: 0,
),
body: Column(
children: <Widget>[
// the tab bar with two items
SizedBox(
height: 50,
child: AppBar(
bottom: const TabBar(
tabs: [
Tab(
text: 'Paid Diet Plans',
),
Tab(
text: 'Personal Exercise',
),
],
),
),
),

// create widgets for each tab bar here
Expanded(
child: TabBarView(
children: [
// first tab bar view widget
Container(
color: Colors.white,
child: ListView(
padding: EdgeInsets.all(8),
children: <Widget>[
Card(
child: ListTile(
title: Text("30 days plan"),
iconColor: Colors.blue,
subtitle: Text('Meal Plan: Burn 1,200 Calories'),
leading: Icon(Icons.sports_gymnastics),
onTap: () {
routeScreen('30 days plan');
},
),
),
Card(
child: ListTile(
title: Text("28 days plan"),
iconColor: Colors.blue,
subtitle: Text('Meal Plan: Burn 950 Calories'),
leading: Icon(Icons.sports_gymnastics),
onTap: () {
routeScreen('28 days plan');
},
),
),
Card(
child: ListTile(
title: Text("22 days plan"),
iconColor: Colors.blue,
subtitle: Text('Meal Plan: Burn 850 Calories'),
leading: Icon(Icons.sports_gymnastics),
onTap: () {
routeScreen('22 days plan');
},
),
),
Card(
child: ListTile(
iconColor: Colors.blue,
title: Text("18 days plan"),
subtitle: Text('Meal Plan: Burn 650 Calories'),
leading: Icon(Icons.sports_gymnastics),
onTap: () {
routeScreen('18 days plan');
},
),
),
],
),
),

// second tab bar viiew widget
Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
childAspectRatio: 3 / 3,
crossAxisSpacing: 20,
mainAxisSpacing: 20),
itemCount: gridItems.length,
itemBuilder: (BuildContext ctx, index) {
return Center(
child: GestureDetector(
onTap: () {
print(' ' + gridItems[index]);
checkDeviceEnvirnoment();
//makePayment();
//checkSDK();
},
child: Container(
alignment: Alignment.center,
child: Image.asset('images/image$index.png'),
decoration: BoxDecoration(
color: Colors.primaries[Random()
.nextInt(Colors.primaries.length)],
borderRadius: BorderRadius.circular(15)),
),
),
);
}),
),
),
],
),
),
],
),
),
);
}

void routeScreen(String title) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => PaidPlanScreen(title)),
);
}

Future<void> fetchRemoteConfiguration() async {
await AGCRemoteConfig.instance
.fetch()
.catchError((error) => print(error.toString()));
await AGCRemoteConfig.instance.applyLastFetched();
Map value = await AGCRemoteConfig.instance.getMergedAll();
final data = jsonDecode(value["categories"].toString());
List<String> gridItem = List<String>.from(data['categories']);
setState(() {
gridItems.addAll(gridItem);
});
}

void checkDeviceEnvirnoment() async {
try {
//Call the isEnvReady API.
IsEnvReadyResult result = await IapClient.isEnvReady();
print('*****');
print(result.returnCode.toString());
//Call isSandboxActivated API.
IsSandboxActivatedResult result1 = await IapClient.isSandboxActivated();
print('####');
//Print the isSandboxUser property.
print(result1.isSandboxUser.toString());
bool? isUser = result1.isSandboxUser;
if (isUser!) {
makePayment();
}

} on Exception catch (_, e) {
print(e.toString());
}
}

void makePayment() async {
try {
//Constructing the request.
PurchaseIntentReq request = PurchaseIntentReq(
productId: '11223344', priceType: IapClient.IN_APP_CONSUMABLE);
request.priceType =
0; //You may also use IN_APP_CONSUMABLE from IapClient's constant values.
request.productId = "11223344";
request.developerPayload = "Test";
//Call the createPurchaseIntent API.
PurchaseResultInfo result = await IapClient.createPurchaseIntent(request);
print(result.inAppPurchaseData.toString());
} on Exception catch (e) {
print(e.toString());
}
}
}

[B][B][B][B]

Result

Using Remote Configuration fetching cloud data and displayed in UI

Before you use any service in AppGallery Console, enable the service first like shown below :

IAP allows three types products to add in AppGallery Console once you click navigate below shown path click add products and choose the desired product type and add details and save, after adding Activate the product then only it will be accessible.

Tricks and Tips

Make sure that downloaded plugin is unzipped in parent directory of project.

Makes sure that agconnect-services.json file added.

Make sure dependencies are added yaml file.

Run flutter pug get after adding dependencies.

Make sure that service is enabled in agc.

Makes sure images are defined in yaml file.

Add products in AppGallery My apps/Operate/Product Management

Conclusion

In this article, we have learnt how to integrate Huawei Remote Configuration service, Account kit, Crash kit flutter DietApp. Once Account kit integrated, users can login quickly and conveniently sign in to apps with their Huawei IDs after granting initial access permission. Push kit enables you to send push notification to user device in real time, you can see the push notification in the result part. Huawei Crash service which provides developers to quickly detect, locate and fix the crash or unexpected exit of application. Hence improves stability and reliability of application. With the help of Remote Configuration service we are fetching the Personal Exercise data from the cloud. Using Huawei IAP we are making payment for access personal exercise.

Thank you so much for reading. I hope this article helps you to understand the integration of Huawei Remote Configuration, Account kit, Huawei IAP and Crash kit in flutter DietApp.

Reference

Crash Kit

Account Kit – Training Video

Crash Kit – Training Video

IAP

Checkout in forum