[Huawei Developers] Sabrina Cara:
View attachment 5232405

Introduction

Hello again my fellow HMS enthusiasts, long time no see…or talk…or write / read… you know what I mean. My new article is about integrating one of Huawei’s kits, namely Site Kit in a project using Clean Architecture and MVVM to bring the user a great experience whilst making it easy for the developers to test and maintain the application.

Before starting with the project, we have to dwell into the architecture of the project in order not to get confused later on when checking the separation of the files.

Clean Architecture

The software design behind Clean Architecture aims to separate the design elements such that the organization of the levels is clean and easy to develop, maintain or test, and where the business logic is completely encapsulated.

The design elements are split into circle layers and the most important rule is the outward dependency rule, stating that the inner layers functionalities have no dependency on the outer ones. The Clean Architecture adaption I have chosen to illustrate is the simple app, data, domain layer, outward in.

The domain layer is the inner layer of the architecture, where all the business logic is maintained, or else the core functionality of the code and it is completely encapsulated from the rest of the layers since it tends to not change throughout the development of the code. This layer contains the Entities, Use Cases and Repository Interfaces.

The middle circle or layer is the data, containing Repository Implementations as well and Data Sources and it depends on the Domain layer.

The outer layer is the app layer, or presentation of the application, containing Activities and Fragments modeled by View Models which execute the use cases of the domain layer. It depends on both data and domain layer.

The work flow of the Clean Architecture using MVVM (Model-View-Viewmodel) is given as follows:

The fragments used call certain methods from the Viewmodels.

The Viewmodels execute the Use Cases attached to them.

The Use Case makes use of the data coming from the repositories.

The Repositories return the data from either a local or remote Data Source.

From there the data returns to the User Interface through Mutable Live Data observation so we can display it to the user. Hence we can tell the data goes through the app ring to the data ring and then all the way back down.

Now that we have clarified the Clean Architecture we will be passing shorty to MVVM so as to make everything clearer on the reader.

MVVM Architecture

This is another architecture used with the aim of facilitating the developers work and separating the development of the graphical interface. It consists in Model-View-Viewmodel method which was shortly mentioned in the previous sections.

This software pattern consists in Views, ViewModels and Models (duhhh how did I come up with that?!). The View is basically the user interface, made up of Activities and Fragments supporting a set of use cases and it is connected through DataBinding to the ModelView which serves as a intermediate between the View and the Model, or else between the UI and the back logic to all the use cases and methods called in the UI.

Why did I choose MVVM with Clean Architecture? Because when projects start to increase in size from small to middle or expand to bigger ones, then the separation of responsibilities becomes harder as the codebase grows huge, making the project more error prone thus increasing the difficulty of the development, testing and maintenance.

With these being said, we can now move on to the development of Site Kit using Clean Architecture + MVVM.

Site Kit

Before you are able to integrate Site Kit, you should create a application and perform the necessary configurations by following this post. Afterwards we can start.

Site Kit is a site service offered by Huawei to help users find places and points of interest, including but not limited to the name of the place, location and address. It can also make suggestions using the autocomplete function or make use of the coordinates to give the users written address and time zone. In this scenario, we will search for restaurants based on the type of food they offer, and we include 6 main types such as burger, pizza, taco, kebab, coffee and dessert.

Now since there is no function in Site Kit that allows us to make a search of a point of interest (POI) based on such types, we will instead conduct a text search where the query will be the type of restaurant we have picked. In the UI or View we call this function with the type of food passed as an argument.

type = args.type.toString()
type?.let { viewModel.getSitesWithKeyword(type,41.0082,28.9784) }

Since we are using MVVM, we will need the ViewModel to call the usecase for us hence in the ViewModel we add the following function and invoke the usecase, to then proceed getting the live data that will come as a response when we move back up in the data flow.

class SearchInputViewModel @ViewModelInject constructor(
private val getSitesWithKeywordUseCase: GetSitesWithKeywordUseCase
) : BaseViewModel() {
private val _restaurantList = MutableLiveData<ResultData<List<Restaurant>>>()
val restaurantList: LiveData<ResultData<List<Restaurant>>>
get() = _restaurantList
@InternalCoroutinesApi
fun getSitesWithKeyword(keyword: String, latitude: Double, longitude: Double) {
viewModelScope.launch(Dispatchers.IO) {
getSitesWithKeywordUseCase.invoke(keyword, latitude, longitude).collect { it ->
handleTask(it) {
_restaurantList.postValue(it)
}
}
}
}
companion object {
private const val TAG = "SearchInputViewModel"
}
}

After passing the app layer of the onion we will now call the UseCase implemented in the domain side where we inject the Site Repository interface so that the UseCase can make use of the data flowing in from the Repository.

class GetSitesWithKeywordUseCase @Inject constructor(private val repository: SitesRepository) {
suspend operator fun invoke(keyword:String, lat: Double, lng: Double): Flow<ResultData<List<Restaurant>>> {
return repository.getSitesWithKeyword(keyword,lat,lng)
}
}

interface SitesRepository {
suspend fun getSitesWithKeyword(keyword:String, lat:Double, lng: Double): Flow<ResultData<List<Restaurant>>>
}

The interface of the Site Repository in the domain actually represents the implemented Site Repository in the data layer which returns data from the remote Sites DataSource using an interface and uses a mapper to map the Site Results to a data class of type Restaurant (since we are getting the data of the Restaurants).

@InternalCoroutinesApi
class SitesRepositoryImpl @Inject constructor(
private val sitesRemoteDataSource: SitesRemoteDataSource,
private val restaurantMapper: Mapper<Restaurant, Site>
) :
SitesRepository {
override suspend fun getSitesWithKeyword(keyword: String,lat:Double, lng:Double): Flow<ResultData<List<Restaurant>>> =
flow {
emit(ResultData.Loading())
val response = sitesRemoteDataSource.getSitesWithKeyword(keyword,lat,lng)
when (response) {
is SitesResponse.Success -> {
val sites = response.data.sites.orEmpty()
val restaurants = restaurantMapper.mapToEntityList(sites)
emit(ResultData.Success(restaurants))
Log.d(TAG, "ResultData.Success emitted ${restaurants.size}")
}
is SitesResponse.Error -> {
emit(ResultData.Failed(response.errorMessage))
Log.d(TAG, "ResultData.Error emitted ${response.errorMessage}")
}
}
}
companion object {
private const val TAG = "SitesRepositoryImpl"
}
}

The SitesRemoteDataSource interface in fact only serves an an interface for the implementation of the real data source (SitesRemoteDataSourceImpl) and gets the SiteResponse coming from it.

interface SitesRemoteDataSource {
suspend fun getSitesWithKeyword(keyword:String, lat:Double, lng:Double): SitesResponse<TextSearchResponse>
}

@ExperimentalCoroutinesApi
class SitesRemoteDataSourceImpl @Inject constructor(private val sitesService: SitesService) :
SitesRemoteDataSource {
override suspend fun getSitesWithKeyword(keyword: String, lat: Double, lng: Double): SitesResponse<TextSearchResponse> {
return sitesService.getSitesByKeyword(keyword,lat,lng)
}
}

We rollback to the Repository in case of any response; in case of success specifically, meaning we got an answer of type Restaurant List from the SiteResponse, we map those results to the data class Restaurant and then keep rolling the data flow back up until we can observe them in the ViewModel through the Mutable Live Data and then display them in the UI Fragment.

sealed class SitesResponse<T> {
data class Success <T>(val data: T) : SitesResponse<T>()
data class Error<T>(val errorMessage: String , val errorCode:String) : SitesResponse<T>()
}

However, before we start rolling back, in order to even be able to get a SiteResponse, we should implement the framework SiteService where we make the necessary API request, in our case the TextSearchRequest by injecting the Site Kit’s Search Service and inserting the type of food the user chose as a query and Restaurant as a POI type.

@ExperimentalCoroutinesApi
class SitesService @Inject constructor(private val searchService: SearchService) {
suspend fun getSitesByKeyword(keyword: String, lat: Double, lng: Double) =
suspendCoroutine<SitesResponse<TextSearchResponse>> { continuation ->
val callback = object : SearchResultListener<TextSearchResponse> {
override fun onSearchResult(p0: TextSearchResponse) {
continuation.resume(SitesResponse.Success(data = p0))
Log.d(
TAG,
"SitesResponse.Success ${p0.totalCount} emitted to flow controller"
)
}
override fun onSearchError(p0: SearchStatus) {
continuation.resume(
SitesResponse.Error(
errorCode = p0.errorCode,
errorMessage = p0.errorMessage
)
)
Log.d(TAG, "SitesResponse.Error emitted to flow controller")
}
}
val request = TextSearchRequest()
val locationIstanbul = Coordinate(lat, lng)
request.apply {
query = keyword
location = locationIstanbul
hwPoiType = HwLocationType.RESTAURANT
radius = 1000
pageSize = 20
pageIndex = 1
}
searchService.textSearch(request, callback)
}
companion object {
const val TAG = "SitesService"
}
}

After making the Text Search Request, we get the result from the callback as a SiteResponse and then start the dataflow back up by passing the SiteResponse to the DataSource, from there to the Respository, then to the UseCase and then finally we observe the data live in the ViewModel, to finally display it in the fragment / UI.

For a better understanding of how the whole project is put together I have prepared a small demo showing the flow of the process.

Site Kit with Clean Architecture and MVVM Demo

And that was it, looks complicated but it really is pretty easy once you get the hang of it. Give it a shot!

Tips and Tricks

Tips are important here as all this process might look confusing at a first glance, so what I would suggest is:

Follow the Clean Architecture structure of the project by splitting your files in separate folders according to their function.

Use Coroutines instead of threads since they are faster and lighter to run.

Use dependency injections (Hilt, Dagger) so as to avoid the tedious job of manual dependency injection for every class.

Conclusion

In this article, we got to mention the structure of Clean Architecture and MVVM and their importance when implemented together in medium / big size projects. We moved on in the implementation of Site Kit Service using the aforementioned architectures and explaining the process of it step by step, until we retrieved the final search result. I hope you try it and like it. As always, stay healthy my friends and see you in other articles.

Reference

HMS Site Kit

Clean Architecture

MVVM with Clean Architecture