DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Medallion Architecture: Why You Need It and How To Implement It With ClickHouse
  • How to Build a React Native Chat App for Android
  • Build a Scalable E-commerce Platform: System Design Overview
  • Using Jetpack Compose With MVI Architecture

Trending

  • Customer 360: Fraud Detection in Fintech With PySpark and ML
  • Understanding IEEE 802.11(Wi-Fi) Encryption and Authentication: Write Your Own Custom Packet Sniffer
  • AI-Driven Root Cause Analysis in SRE: Enhancing Incident Resolution
  • Mastering Advanced Aggregations in Spark SQL
  1. DZone
  2. Data Engineering
  3. Data
  4. Android App Architecture [Part 4] Presentation Layer

Android App Architecture [Part 4] Presentation Layer

By 
Radivoje Ostojic user avatar
Radivoje Ostojic
DZone Core CORE ·
Dec. 10, 19 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
12.3K Views

Join the DZone community and get the full member experience.

Join For Free

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
}

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

  • Java Vs. Kotlin: Which One Will Be the Best in 2019?
  • Android Tutorial – Learn Android From Scratch!
Android (robot) app Architecture View model Data (computing)

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

Opinions expressed by DZone contributors are their own.

Related

  • Medallion Architecture: Why You Need It and How To Implement It With ClickHouse
  • How to Build a React Native Chat App for Android
  • Build a Scalable E-commerce Platform: System Design Overview
  • Using Jetpack Compose With MVI Architecture

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!