Expert: Integration of HMS Core Kits in Android Jetpack App Part-1

Overview

In this article, I will create an Android Jetpack based Recipe App in which I will integrate HMS Core kits such as Huawei ID, Huawei Ads and much more.

In this series of article I will cover all the kits with real life usages in this application. This is the part-1 article of this series.

Huawei ID Service Introduction

Huawei ID login provides you with simple, secure, and quick sign-in and authorization functions. Instead of entering accounts and passwords and waiting for authentication, users can just tap the Sign in with HUAWEI ID button to quickly and securely sign in to your app with their HUAWEI IDs.

Interstitial Ads Introduction

Interstitial ads are full-screen ads that covers the interface of an app. Such as ad is displayed when a user starts, pauses, or exits an app, without disrupting the user’s experience.

Prerequisite

  1. Huawei Phone EMUI 3.0 or later.
  2. Non-Huawei phones Android 4.4 or later (API level 19 or higher).
  3. HMS Core APK 4.0.0.300 or later.
  4. Android Studio
  5. AppGallery Account

App Gallery Integration process

  1. Sign In and Create or Choose a project on AppGallery Connect portal.
  2. Navigate to Project settings and download the configuration file.
  3. Navigate to General Information, and then provide Data Storage location.

App Development

  1. Create A New Project.
  2. Configure Project Gradle.
  • repositories { google() jcenter() maven { url 'http://developer.huawei.com/repo/' } } dependencies { classpath 'com.android.tools.build:gradle:4.0.1' classpath 'com.huawei.agconnect:agcp:1.2.1.301' }
  1. Configure App Gradle.
  1. Configure AndroidManifest.xml.

Jetpack Components Implementation

· Data Binding: Declaratively bind UI elements to in our layout to data sources of our app.

· Lifecycles: Manages activity and fragment lifecycles of our app.

· LiveData: Notify views of any database changes.

· Room: Fluent SQLite database access.

· ViewModel: Manage UI-related data in a lifecycle-conscious way.

import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MediatorLiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Observer;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.List;public class RecipeListViewModel extends AndroidViewModel { private static final String TAG = "RecipeListViewModel"; public static final String QUERY_EXHAUSTED = "No more results.";
public enum ViewState {CATEGORIES, RECIPES}
private MutableLiveData<ViewState> viewState;
private MediatorLiveData<Resource<List<Recipe>>> recipes = new MediatorLiveData<>();
private RecipeRepository recipeRepository;
// query extras
private boolean isQueryExhausted;
private boolean isPerformingQuery;
private int pageNumber;
private String query;
private boolean cancelRequest;
private long requestStartTime;
public RecipeListViewModel(@NonNull Application application) {
super(application);
recipeRepository = RecipeRepository.getInstance(application);
init();
} private void init(){
if(viewState == null){
viewState = new MutableLiveData<>();
viewState.setValue(ViewState.CATEGORIES);
}
}
public LiveData<ViewState> getViewstate(){
return viewState;
}
public LiveData<Resource<List<Recipe>>> getRecipes(){
return recipes;
}
public int getPageNumber(){
return pageNumber;
}
public void setViewCategories(){
viewState.setValue(ViewState.CATEGORIES);
}
public void searchRecipesApi(String query, int pageNumber){
if(!isPerformingQuery){
if(pageNumber == 0){
pageNumber = 1;
}
this.pageNumber = pageNumber;
this.query = query;
isQueryExhausted = false;
executeSearch();
}
}
public void searchNextPage(){
if(!isQueryExhausted && !isPerformingQuery){
pageNumber++;
executeSearch();
}
}
private void executeSearch(){
requestStartTime = System.currentTimeMillis();
cancelRequest = false;
isPerformingQuery = true;
viewState.setValue(ViewState.RECIPES);
final LiveData<Resource<List<Recipe>>> repositorySource = recipeRepository.searchRecipesApi(query, pageNumber);
recipes.addSource(repositorySource, new Observer<Resource<List<Recipe>>>() {
@Override
public void onChanged(@Nullable Resource<List<Recipe>> listResource) {
if(!cancelRequest){
if(listResource != null){
if(listResource.status == Resource.Status.SUCCESS){
Log.d(TAG, "onChanged: REQUEST TIME: " + (System.currentTimeMillis() - requestStartTime) / 1000 + " seconds.");
Log.d(TAG, "onChanged: page number: " + pageNumber);
Log.d(TAG, "onChanged: " + listResource.data);
isPerformingQuery = false;
if(listResource.data != null){
if(listResource.data.size() == 0 ){
Log.d(TAG, "onChanged: query is exhausted...");
recipes.setValue(
new Resource<List<Recipe>>(
Resource.Status.ERROR,
listResource.data,
QUERY_EXHAUSTED
)
);
isQueryExhausted = true;
}
}
recipes.removeSource(repositorySource);
}
else if(listResource.status == Resource.Status.ERROR){
Log.d(TAG, "onChanged: REQUEST TIME: " + (System.currentTimeMillis() - requestStartTime) / 1000 + " seconds.");
isPerformingQuery = false;
if(listResource.message.equals(QUERY_EXHAUSTED)){
isQueryExhausted = true;
}
recipes.removeSource(repositorySource);
}
recipes.setValue(listResource);
}
else{
recipes.removeSource(repositorySource);
}
}
else{
recipes.removeSource(repositorySource);
}
}
});
}
public void cancelSearchRequest(){
if(isPerformingQuery){
Log.d(TAG, "cancelSearchRequest: canceling the search request.");
cancelRequest = true;
isPerformingQuery = false;
pageNumber = 1;
}
}
}

Activity

public class RecipeListActivity extends BaseActivity implements OnRecipeListener {    private static final String TAG = "RecipeListActivity";    private RecipeListViewModel mRecipeListViewModel;
private RecyclerView mRecyclerView;
private RecipeRecyclerAdapter mAdapter;
private SearchView mSearchView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recipe_list);
mRecyclerView = findViewById(R.id.recipe_list);
mSearchView = findViewById(R.id.search_view);
mRecipeListViewModel = ViewModelProviders.of(this).get(RecipeListViewModel.class); initRecyclerView();
initSearchView();
subscribeObservers();
setSupportActionBar((Toolbar)findViewById(R.id.toolbar));
}
private void subscribeObservers(){
mRecipeListViewModel.getRecipes().observe(this, new Observer<Resource<List<Recipe>>>() {
@Override
public void onChanged(@Nullable Resource<List<Recipe>> listResource) {
if(listResource != null){
Log.d(TAG, "onChanged: status: " + listResource.status);
if(listResource.data != null){
switch (listResource.status){
case LOADING:{
if(mRecipeListViewModel.getPageNumber() > 1){
mAdapter.displayLoading();
}
else{
mAdapter.displayOnlyLoading();
}
break;
}
case ERROR:{
Log.e(TAG, "onChanged: cannot refresh the cache." );
Log.e(TAG, "onChanged: ERROR message: " + listResource.message );
Log.e(TAG, "onChanged: status: ERROR, #recipes: " + listResource.data.size());
mAdapter.hideLoading();
mAdapter.setRecipes(listResource.data);
Toast.makeText(RecipeListActivity.this, listResource.message, Toast.LENGTH_SHORT).show();
if(listResource.message.equals(QUERY_EXHAUSTED)){
mAdapter.setQueryExhausted();
}
break;
}
case SUCCESS:{
Log.d(TAG, "onChanged: cache has been refreshed.");
Log.d(TAG, "onChanged: status: SUCCESS, #Recipes: " + listResource.data.size());
mAdapter.hideLoading();
mAdapter.setRecipes(listResource.data);
break;
}
}
}
}
}
});
mRecipeListViewModel.getViewstate().observe(this, new Observer<RecipeListViewModel.ViewState>() {
@Override
public void onChanged(@Nullable RecipeListViewModel.ViewState viewState) {
if(viewState != null){
switch (viewState){
case RECIPES:{
// recipes will show automatically from other observer
break;
}
case CATEGORIES:{
displaySearchCategories();
break;
}
}
}
}
});
}
private RequestManager initGlide(){ RequestOptions options = new RequestOptions()
.placeholder(R.drawable.white_background)
.error(R.drawable.white_background);
return Glide.with(this)
.setDefaultRequestOptions(options);
}
private void searchRecipesApi(String query){
mRecyclerView.smoothScrollToPosition(0);
mRecipeListViewModel.searchRecipesApi(query, 1);
mSearchView.clearFocus();
}
private void initRecyclerView(){
ViewPreloadSizeProvider<String> viewPreloader = new ViewPreloadSizeProvider<>();
mAdapter = new RecipeRecyclerAdapter(this, initGlide(), viewPreloader);
VerticalSpacingItemDecorator itemDecorator = new VerticalSpacingItemDecorator(30);
mRecyclerView.addItemDecoration(itemDecorator);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
RecyclerViewPreloader<String> preloader = new RecyclerViewPreloader<String>(
Glide.with(this),
mAdapter,
viewPreloader,
30);
mRecyclerView.addOnScrollListener(preloader); mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if(!mRecyclerView.canScrollVertically(1)
&& mRecipeListViewModel.getViewstate().getValue() == RecipeListViewModel.ViewState.RECIPES){
mRecipeListViewModel.searchNextPage();
}
}
});
mRecyclerView.setAdapter(mAdapter);
}
private void initSearchView(){
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
searchRecipesApi(s);
return false;
}
@Override
public boolean onQueryTextChange(String s) {
return false;
}
});
}
@Override
public void onRecipeClick(int position) {
Intent intent = new Intent(this, RecipeActivity.class);
intent.putExtra("recipe", mAdapter.getSelectedRecipe(position));
startActivity(intent);
}
@Override
public void onCategoryClick(String category) {
searchRecipesApi(category);
}
private void displaySearchCategories(){
mAdapter.displaySearchCategories();
}
@Override
public void onBackPressed() {
if(mRecipeListViewModel.getViewstate().getValue() == RecipeListViewModel.ViewState.CATEGORIES){
super.onBackPressed();
}
else{
mRecipeListViewModel.cancelSearchRequest();
mRecipeListViewModel.setViewCategories();
}
}
}

App Build Result

Tips and Tricks

Identity Kit displays the HUAWEI ID registration or sign-in page first. The user can use the functions provided by Identity Kit only after signing in using a registered HUAWEI ID.

If you are using a device of the Chinese mainland version, which is connected to the Internet in the Chinese mainland, only these two banner ad dimensions are supported.

Conclusion

In this article, we have learned how to integrate Huawei ID and Ads in Android application. After completely read this article user can easily implement Huawei ID and Ads in the application.

Thanks for reading this article. Be sure to like and comment to this article, if you found it helpful. It means a lot to me.

References

HMS Docs:

https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/introduction-0000001050048870

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store