In previous articles, I described and implemented the data layer, domain layer, and data and domain modules for one of the features in our application 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 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 }
This module should implement necessary dependencies from the core module, and it’ll depend on the domain_weather module.
After we finish 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 data class WeatherView inside it.
package com.rostojic.weather.model import android.os.Parcelable import kotlinx.android.parcel.Parcelize @Parcelize data 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:
Besides this main screen, we’ll have one more screen for loading and error. 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 go for now. The next step is to create two fragments, one for loading and one for displaying 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 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:
Inside weather_navigation.xml for DisplayWeatherFragment add argument for WeatherView; we need to pass WeatherView object to 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 the WeatherView instance to the DisplayWeatherFragment first, 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’ll run GetWeatherUseCase and return Weather instance or error.
fun getWeatherData() { getWeatherUseCase.run { clear() executeUseCase { handleResult(it) } } }
Before we create handleResult() function, we need to add a constant error string that’ll be used in both view model and fragment to identify connection error:
companion object { const val CONNECTION_ERROR = "connection_error" }
Now we can implement 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() }
And the last but important thing is to call clear on getWeatherUseCase() in overridden method onCleared():
override fun onCleared() { super.onCleared() getWeatherUseCase.clear() }
Before we implement LoadWeatherFragment we need to setup a dependency injection for this module because the first thing in LoadWeatherFragment will be injecting 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 the same package creates 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 is to create WeatherInjector and call it from 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(). The next step is to 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)) }
Function weatherReceived 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 result’s loading :
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 connection error view otherwise show 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 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. Final step is to extend 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.