{{announcement.body}}
{{announcement.title}}

Android App Architecture [Part 4] Presentation Layer

DZone 's Guide to

Android App Architecture [Part 4] Presentation Layer

In this article, we discuss the Presentation layer of Android application architecture and create a UI for our WeatherApp application.

· Web Dev Zone ·
Free Resource

man-giving-presentation

In the previous articles, I described and implemented the data layer, domain layer, and data and domain modules for one of the features in our application, the WeatherApp. Now, it's time to implement the last missing layer of our app, the presentation layer.

What Is the Presentation Layer?

As the name suggests, this layer is responsible for presenting UI to the user. It's used to perform necessary UI logic based on the data received through the domain layer and user interactions. In the previous article, we described and implemented our domain layer. Our presentation layer depends on it, but the domain shouldn't know anything about the presentation layer. This presentation layer follows the MVVM architecture pattern that we described in our first article. Also, we'll use Android Jetpack to implement this pattern correctly.

First, we need to create a presentation module for our weather feature. In the directory feature, create the module feature_weather. After that, the first step is to update Gradle dependency:

dependencies { 
  implementation project(':core') 
  implementation project(':domain_weather')  

  kapt deps.dagger.daggerCompiler
}


You may also like: Android App Architecture: Modularization, Clean Architecture, MVVM.

This module should implement necessary dependencies from the core module, and it'll depend on the domain_weather module.

After we finish creating our Gradle file, the next step is to create a presentation model. This presentation model will be mapped from the domain model. Create a package called model inside main/java and a data class, WeatherView, inside it.

package com.rostojic.weather.model

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelizedata 
class WeatherView( 
  val city: String, 
  val dateTime: String, 
  val weatherImage: String, 
  val temperature: String, 
  val feelsLike: String, 
  val description: String, 
  val precipitation: String, 
  val uvIndex: String
  ) : Parcelable
  
  fun Weather.mapToView(): WeatherView = WeatherView( 
  	this.city, 
  	this.dateTime, 
  	this.weatherImage, 
  	this.temperature, 
  	this.feelsLike, 
  	this.description, 
  	this.precipitation, 
  	this.uvIndex
  )


To parcel this WeatherView data class, we need to modify the build.gradle of our presentation module. Below android and abode dependency add androidExtensions and set experimental to true:

androidExtensions { 
  experimental = true
}


Now, when we have our presentation model, we can create a design. The design won't be complicated because it's not an essential part of our application. We'll implement a simple screen using ConstraintLayout to display data from the model:


Initial UI
Besides this main screen, we'll have one more screen for loading and errors. Create a new resource file called load_weather_fragment.xml:

<include
 android:id="@+id/viewLoading"
 layout="@layout/view_loading"
 android:layout_width="match_parent"
 android:layout_height="match_parent" />
   
<include
 android:id="@+id/viewError"
 layout="@layout/view_error"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:visibility="gone" />


This layout should include two views, view_loading, and view_error. The loading view should have just one rotating progress bar, and an error view should have a simple image viewer with an error image. Set error_view to gone for now.

The next step is to create two fragments, one for loading and one for displaying the weather. First, create a directory called UI under main/java. Inside the UI package, create two more packages, display and load. Inside display, create a file called DisplayWeatherFragment, and inside load create a file, LoadWeatherFragment.

DisplayWeatherFragment:

package com.rostojic.weather.ui.display

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.rostojic.weather.R

class DisplayWeatherFragment : Fragment() {
 override fun onCreateView(
 inflater: LayoutInflater,
 container: ViewGroup?,
 savedInstanceState: Bundle?
 ): View? {
 return inflater.inflate(R.layout.display_weather_fragment, container, false)
 }
 
 override fun onActivityCreated(savedInstanceState: Bundle?) {
 super.onActivityCreated(savedInstanceState)
 setUpViews()
 }
 private fun setUpViews() {}
 
}

LoadWeatherFragment:

package com.rostojic.weather.ui.load

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.rostojic.weather.R

class LoadWeatherFragment : Fragment() {
 override fun onCreateView(
 inflater: LayoutInflater,
 container: ViewGroup?,
 savedInstanceState: Bundle?
 ): View? {
 return inflater.inflate(R.layout.load_weather_fragment, container, false)
 }
 
 override fun onActivityCreated(savedInstanceState: Bundle?) {
 super.onActivityCreated(savedInstanceState)
 
 loadWeather()
 }
 
 private fun loadWeather() {}
}


Now, when we have our fragments, we can create a navigation resource file on the app or feature level. For now, we'll create it on the app-level because we'll have only two screens in our application for now. In the Weatherapp module, create a navigation resource file called weather_navigation.xml, and inside it, set our navigation graph:
Loading and main views

Inside weather_navigation.xml for DisplayWeatherFragment add an argument for WeatherView. We need to pass a WeatherView object to the DisplayWeatherFragment  screen once it's loaded to display weather data to the user:

<argument
 android:name="weatherView"
 app:argType="com.rostojic.weather.model.WeatherView"
 app:nullable="false" />


To pass a WeatherView instance to the DisplayWeatherFragment, we need to load it from the domain layer. For loading and direct communication to the domain layer, we'll create a view model class inside the load package called LoadWeatherViewModel. First, we'll inject GetWeatherUseCase from the domain layer through the constructor:

class LoadWeatherViewModel @Inject constructor(
 private val getWeatherUseCase: GetWeatherUseCase
) : ViewModel()


To properly implement the observer pattern, we'll use a lifecycle-aware data holder from Android Jetpack, LiveData. Before we use it in our view model, we need to create a LiveData extension in our core module. Inside the core module, under package extensions, create a new file called LiveDataExtensions and add those two extensions:

fun <T> MutableLiveData<Resource<T>>.setSuccess(data: T) =
 postValue(Resource(Resource.State.Success, data))
  
fun <T> MutableLiveData<Resource<T>>.setError(message: String? = null) =
 postValue(Resource(Resource.State.Error, value?.data, message))


Once we add those extensions, we can create LiveData for getting weather information in LoadWeatherViewModel:

private var _getWeatherLiveData = MutableLiveData<Resource<WeatherView>>()
val getWeatherLiveData: LiveData<Resource<WeatherView>>
 get() = _getWeatherLiveData


The next step is to create a public function that will run GetWeatherUseCase and return a Weather  instance or error.

fun getWeatherData() {
 getWeatherUseCase.run {
 clear()
 executeUseCase { handleResult(it) }
 }
}


Before we create the handleResult() function, we need to add a constant error string that'll be used in both view model and fragment to identify a connection error:

companion object {
 const val CONNECTION_ERROR = "connection_error"
}


Now, we can implement the handleResult() function:

private fun handleResult(status: GetWeatherUseCase.Status) {
 when (status) {
 is GetWeatherUseCase.Status.Success -> onGetWeatherSuccess(status)
 is GetWeatherUseCase.Status.ConnectionError -> onGetWeatherConnectionError()
 is GetWeatherUseCase.Status.UnknownError -> onGetWeatherUnknownError()
 }
}

private fun onGetWeatherSuccess(status: GetWeatherUseCase.Status.Success) {
 _getWeatherLiveData.setSuccess(status.weather.mapToView())
}

private fun onGetWeatherConnectionError() {
 _getWeatherLiveData.setError(CONNECTION_ERROR)
}

private fun onGetWeatherUnknownError() {
 _getWeatherLiveData.setError()
}


The last but important thing is to call clear on getWeatherUseCase() in the overridden method,  onCleared():

override fun onCleared() {
 super.onCleared()
 getWeatherUseCase.clear()
}


Before we implement LoadWeatherFragment, we need to set up dependency injection for this module because the first thing we do in LoadWeatherFragment will be injecting the LoadWeatherViewModel. Under main/java, create a package called di, and under it, create the first file called WeatherScope:

package com.rostojic.weather.di

import javax.inject.Scope

@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class WeatherScope


After WeatherScope, in same package, create a file called WeatherComponent:

package com.rostojic.weather.di

import com.rostojic.core.presentation.ViewModelFactory
import com.rostojic.weather.ui.load.LoadWeatherViewModel
import dagger.Component

@WeatherScope
@Component(dependencies = [WeatherDomainComponent::class])
interface WeatherComponent {
  
 fun getWeatherViewModelFactory(): ViewModelFactory<LoadWeatherViewModel>
}


This component depends on the domain component. Next, create a WeatherInjector and call it from the Weatherapp module:

package com.rostojic.weather.di
object WeatherInjector {
 
 lateinit var component: WeatherComponent
 
 fun initialise() {
 
 component =
 
 DaggerWeatherComponent.builder().weatherDomainComponent(WeatherDomainInjector.component)
 .build()
 }
}


Once we're done with dependency injection, we can continue working on our fragments. Using WetherComponent, we can access our view model like this:

private val weatherViewModel by 
viewModel(WeatherInjector.component.getWeatherViewModelFactory())


Add this line above  onCreateView(). Next, override  onActivityCreated(), and call a public method from the view model for getting weather and method to initialize observer inside it:

private val weatherViewModel by viewModel(WeatherInjector.component.getWeatherViewModelFactory())

/* Add this line above onCreateView(). Next step is to override onActivityCreated() 
and inside it call public method from view model for getting weather and 
method to initialise observer:*/

 override fun onActivityCreated(savedInstanceState: Bundle?) {
 super.onActivityCreated(savedInstanceState)
   
 loadWeather()
 initialiseViewModelObserver()
   
}

private fun loadWeather() {
 weatherViewModel.getWeatherData()
}

private fun initialiseViewModelObserver() {
 weatherViewModel.getWeatherLiveData.observe(this, Observer(::weatherReceived))
}


The weatherReceived function is responsible for performing UI logic based on  Resource.State:

private fun weatherReceived(weatherResource: Resource<WeatherView>) {
 weatherResource.let {
 when (it.state) {
 is Resource.State.Loading -> onWeatherFetchLoading()
 is Resource.State.Success -> onWeatherFetchSuccess(it)
 is Resource.State.Error -> onWeatherFetchError(it)
 }
 }
}


We need to show the loading view while the results load:

private fun onWeatherFetchLoading() {
 viewLoading.visibility = View.VISIBLE
 viewError.visibility = View.GONE
}


When we get an error as a result, we need to preview the error view:

private fun onWeatherFetchError(weatherResource: Resource<WeatherView>) {
 when (weatherResource.message) {
 CONNECTION_ERROR -> setConnectionError()
 else -> setUnknownError()
 }
}


If the error is CONNECTION_ERROR, show the connection error view; otherwise, show the unknown error:

private fun setConnectionError() {
 viewLoading.visibility = View.GONE
 viewError.apply {
 setConnectionError()
 visibility = View.VISIBLE
 }
}

private fun setUnknownError() {
 viewLoading.visibility = View.GONE
 viewError.apply {
 setUnknownError()
 visibility = View.VISIBLE
 }
}


Currently, we don't have different UIs for unknown and connection errors, but it can be easily extended with this logic.

If the result is a success, we'll get a WeatherView instance, navigate to DisplayWeatherFragment, and pass this WeatherView object as an argument:

private fun onWeatherFetchSuccess(weatherResource: Resource<WeatherView>) {
 navigateToWeather(weatherResource.data ?: return)
}

private fun navigateToWeather(weatherView: WeatherView) {
 val bundle = bundleOf("weather" to weatherView)
 findNavController().navigate(
 R.id.action_loadWeatherFragment_to_displayWeatherFragment,
 bundle
 )
}


With this, we're finished with loading data from the view model, and we're done with LoadWeatherFrament. The final step is to extend the function, setUpView(), inside DisplayWeatherFragment to display actual data from our WeatherView object that we received from LoadWeatherFragment:

private fun setUpViews() {
 val weatherView: WeatherView = arguments?.get("weather") as WeatherView
 weatherView.let {
 textCity.text = weatherView.city
 textDateTime.text = weatherView.dateTime
 Glide.with(this).load(weatherView.weatherImage).into(imageWeather)
 textDescription.text = weatherView.description
 textFeelsLike.text = weatherView.feelsLike
 textPercipitation.text = weatherView.precipitation
 textTemperature.text = weatherView.temperature
 textUvIndex.text = weatherView.uvIndex
 }
}


That is all. We have implemented the last layer of our architecture. In this article, we implemented the presentation layer for weather features following the MVVM design pattern and with the help of Android Jetpack.


Further Reading

Topics:
android, app architecture, presentation layer, tutorial, uncle bob, web dev

Published at DZone with permission of Radivoje Ostojic . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}