SOLID principles in practice:
the clean architecture
Fabio Collini
@fabioCollini
linkedin.com/in/fabiocollini
github.com/fabioCollini
medium.com/@fabioCollini
codingjam.it
Android programmazione avanzata
Android Developers Italia
Ego slide
entitiesentities
Clean
architecture
S.O.L.I.D.
principles
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
Coroutines
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
Dependency Injection
class WeatherUseCaseTest {
val locationManager: LocationManager = mockk()
val repository: TemperatureRepository = mockk()
val useCase = WeatherUseCase(locationManager, repository)
@Test
fun retrieveCityData() {
coEvery { locationManager.getLastLocation() } returns LOCATION
coEvery { locationManager.getCities(LOCATION) } returns
listOf(City("Firenze", "IT"))
coEvery { repository.getTemperature(LAT, LON) } returns
Temperature(10, 8, 20)
val cityData = runBlocking { useCase.getCityData() }
assert(cityData).isEqualTo("Firenze (IT) n 10º min 8º max 20º")
}1
}2
class WeatherUseCaseTest {
val locationManager: LocationManager = mockk()
val repository: TemperatureRepository = mockk()
val useCase = WeatherUseCase(locationManager, repository)
@Test
fun retrieveCityData() {
coEvery { locationManager.getLastLocation() } returns LOCATION
coEvery { locationManager.getCities(LOCATION) } returns
listOf(City("Firenze", "IT"))
coEvery { repository.getTemperature(LAT, LON) } returns
Temperature(10, 8, 20)
val cityData = runBlocking { useCase.getCityData() }
assert(cityData).isEqualTo("Firenze (IT) n 10º min 8º max 20º")
}1
}2
class WeatherUseCaseTest {
val locationManager: LocationManager = mockk()
val repository: TemperatureRepository = mockk()
val useCase = WeatherUseCase(locationManager, repository)
@Test
fun retrieveCityData() {
coEvery { locationManager.getLastLocation() } returns LOCATION
coEvery { locationManager.getCities(LOCATION) } returns
listOf(City("Firenze", "IT"))
coEvery { repository.getTemperature(LAT, LON) } returns
Temperature(10, 8, 20)
val cityData = runBlocking { useCase.getCityData() }
assert(cityData).isEqualTo("Firenze (IT) n 10º min 8º max 20º")
}1
}2
class WeatherUseCaseTest {
val locationManager: LocationManager = mockk()
val repository: TemperatureRepository = mockk()
val useCase = WeatherUseCase(locationManager, repository)
@Test
fun retrieveCityData() {
coEvery { locationManager.getLastLocation() } returns LOCATION
coEvery { locationManager.getCities(LOCATION) } returns
listOf(City("Firenze", "IT"))
coEvery { repository.getTemperature(LAT, LON) } returns
Temperature(10, 8, 20)
val cityData = runBlocking { useCase.getCityData() }
assert(cityData).isEqualTo("Firenze (IT) n 10º min 8º max 20º")
}1
}2
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
@SuppressLint("MissingPermission")
override suspend fun getLastLocation(): Location = suspendCoroutine { continuation ->
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
if (location == null)
continuation.resumeWithException(Exception("Location not available"))
else
continuation.resume(Location(location.latitude, location.longitude))
}
.addOnFailureListener {
continuation.resumeWithException(it)
}
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
geocoder.getFromLocation(location.lat, location.lon, 10)
.filter { it.locality != null }
.map {
City(it.locality, it.countryCode)
}
}E
}E
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
interface WeatherApi {
@GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun currentWeather(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): TemperatureWrapper
@GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun forecast(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): Forecast
}Z
interface WeatherApi {
@GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun currentWeather(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): TemperatureWrapper
@GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun forecast(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): Forecast
}Z
class WeatherViewModel(app: Application) : AndroidViewModel(app) {
private val api = RetrofitFactory.createService<WeatherApi>()
private val weatherRepository = OpenWeatherTemperatureRepository(api)
private val positionManager = AndroidLocationManager(app)
private val useCase = WeatherUseCase(positionManager, weatherRepository)
val state = MutableLiveData<String>()
fun load() {
viewModelScope.launch {
val result = useCase.getCityData()
state.value = result
}O
}O
}O
class MainActivity : AppCompatActivity() {
//...
}
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
interface WeatherApi {
@GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun currentWeather(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): TemperatureWrapper
@GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun forecast(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): Forecast
}Z
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
class WeatherViewModel(app: Application) : AndroidViewModel(app) {
private val api = RetrofitFactory.createService<WeatherApi>()
private val weatherRepository = OpenWeatherTemperatureRepository(api)
private val positionManager = AndroidLocationManager(app)
private val useCase = WeatherUseCase(positionManager, weatherRepository)
val state = MutableLiveData<String>()
fun load() {
viewModelScope.launch {
val result = useCase.getCityData()
state.value = result
}O
}O
}O
app
api
domain
weather
location
class MainActivity : AppCompatActivity() {
//...
}
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
class WeatherViewModel(app: Application) : AndroidViewModel(app) {
private val api = RetrofitFactory.createService<WeatherApi>()
private val weatherRepository = OpenWeatherTemperatureRepository(api)
private val positionManager = AndroidLocationManager(app)
private val useCase = WeatherUseCase(positionManager, weatherRepository)
val state = MutableLiveData<String>()
fun load() {
viewModelScope.launch {
val result = useCase.getCityData()
state.value = result
}O
}O
}O
interface WeatherApi {
@GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun currentWeather(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): TemperatureWrapper
@GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun forecast(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): Forecast
}Z
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
Java/Kotlin modules are more…
Testable
Reusable
Robert C. Martin Copyright (c) 2000 by Robert C. Martin. All Rights Reserved.
www.objectmentor.com 1
Design Principles and
Design Patterns
Robert C. Martin
www.objectmentor.com
What is software architecture? The answer is multitiered. At the highest level, there
are the architecture patterns that define the overall shape and structure of software
applications1
. Down a level is the architecture that is specifically related to the pur-
pose of the software application. Yet another level down resides the architecture of
the modules and their interconnections. This is the domain of design patterns2
, pack-
akges, components, and classes. It is this level that we will concern ourselves with in
this chapter.
Our scope in this chapter is quite limitted. There is much more to be said about the
principles and patterns that are exposed here. Interested readers are referred to
[Martin99].
Architecture and Dependencies
What goes wrong with software? The design of many software applications begins as
a vital image in the minds of its designers. At this stage it is clean, elegant, and com-
pelling. It has a simple beauty that makes the designers and implementers itch to see it
working. Some of these applications manage to maintain this purity of design through
the initial development and into the first release.
But then something begins to happen. The software starts to rot. At first it isn’t so
bad. An ugly wart here, a clumsy hack there, but the beauty of the design still shows
through. Yet, over time as the rotting continues, the ugly festering sores and boils
accumulate until they dominate the design of the application. The program becomes a
festering mass of code that the developers find increasingly hard to maintain. Eventu-
1. [Shaw96]
2. [GOF96]
Copyright (c) 2000 by Robert C. Martin. All Rights Reserved.
S
O
L
I
D
Single responsibility
“A class should have one, and only one,
reason to change”
app
domain
weather
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
class WeatherViewModel(app: Application) : AndroidViewModel(app) {
private val api = RetrofitFactory.createService<WeatherApi>()
private val weatherRepository = OpenWeatherTemperatureRepository(api)
private val positionManager = AndroidLocationManager(app)
private val useCase = WeatherUseCase(positionManager, weatherRepository)
val state = MutableLiveData<String>()
fun load() {
viewModelScope.launch {
val result = useCase.getCityData()
state.value = result
}O
}O
}O
class MainActivity : AppCompatActivity() {
//...
}
location
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
api
interface WeatherApi {
@GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun currentWeather(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): TemperatureWrapper
@GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun forecast(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): Forecast
}Z
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
S
O
L
I
D
Open closed
“You should be able to extend a classes
behavior, without modifying it”
interface
impl1 impl2
interface
impl1 impl3impl2
S
O
L
I
D
Liskov substitution
“Derived classes must be substitutable
for their base classes”
open class Rectangle(
open var width: Int,
open var height: Int
) {
fun area() = width * height
}
class Square(size: Int) : Rectangle(size, size) {
override var width: Int = size
set(value) {
field = value
if (height != value)
height = value
}
override var height: Int = size
set(value) {
field = value
if (width != value)
width = value
}
}
var rectangle = Rectangle(6, 4)
var initialArea = rectangle.area()
rectangle = Square(3)
initialArea = rectangle.area()
rectangle.height *= 2
println(initialArea * 2 == rectangle.area())
rectangle.height *= 2
println(initialArea * 2 == rectangle.area())
S
O
L
I
D
Interface segregation
“Make fine grained interfaces
that are client specific”
S
O
L
I
D
Dependency inversion
Dependency injection?
Inversion of control?
Dependency inversion
“Depend on abstractions,
not on concretions”
app
domain
weather
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
class WeatherViewModel(app: Application) : AndroidViewModel(app) {
private val api = RetrofitFactory.createService<WeatherApi>()
private val weatherRepository = OpenWeatherTemperatureRepository(api)
private val positionManager = AndroidLocationManager(app)
private val useCase = WeatherUseCase(positionManager, weatherRepository)
val state = MutableLiveData<String>()
fun load() {
viewModelScope.launch {
val result = useCase.getCityData()
state.value = result
}O
}O
}O
class MainActivity : AppCompatActivity() {
//...
}
location
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
api
interface WeatherApi {
@GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun currentWeather(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): TemperatureWrapper
@GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun forecast(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): Forecast
}Z
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
app
domain
weather
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
class WeatherViewModel(app: Application) : AndroidViewModel(app) {
private val api = RetrofitFactory.createService<WeatherApi>()
private val weatherRepository = OpenWeatherTemperatureRepository(api)
private val positionManager = AndroidLocationManager(app)
private val useCase = WeatherUseCase(positionManager, weatherRepository)
val state = MutableLiveData<String>()
fun load() {
viewModelScope.launch {
val result = useCase.getCityData()
state.value = result
}O
}O
}O
class MainActivity : AppCompatActivity() {
//...
}
location
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
api
interface WeatherApi {
@GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun currentWeather(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): TemperatureWrapper
@GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun forecast(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): Forecast
}Z
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
Dependency inversion
“Depend on abstractions,
not on concretions”
Dependency inversion
“High level modules should not depend
upon low level modules”
Dependency inversion
“High level modules should not depend
upon low level modules”
app
domain
weather
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
class WeatherViewModel(app: Application) : AndroidViewModel(app) {
private val api = RetrofitFactory.createService<WeatherApi>()
private val weatherRepository = OpenWeatherTemperatureRepository(api)
private val positionManager = AndroidLocationManager(app)
private val useCase = WeatherUseCase(positionManager, weatherRepository)
val state = MutableLiveData<String>()
fun load() {
viewModelScope.launch {
val result = useCase.getCityData()
state.value = result
}O
}O
}O
class MainActivity : AppCompatActivity() {
//...
}
location
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
api
interface WeatherApi {
@GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun currentWeather(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): TemperatureWrapper
@GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun forecast(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): Forecast
}Z
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
domain location
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
domain location
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
app
domain
weather
interface TemperatureRepository {
suspend fun getTemperature(lat: Double, lon: Double): Temperature
}7
location
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
class WeatherViewModel(app: Application) : AndroidViewModel(app) {
private val api = RetrofitFactory.createService<WeatherApi>()
private val weatherRepository = OpenWeatherTemperatureRepository(api)
private val positionManager = AndroidLocationManager(app)
private val useCase = WeatherUseCase(positionManager, weatherRepository)
val state = MutableLiveData<String>()
fun load() {
viewModelScope.launch {
val result = useCase.getCityData()
state.value = result
}O
}O
}O
class MainActivity : AppCompatActivity() {
//...
}
api
interface WeatherApi {
@GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun currentWeather(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): TemperatureWrapper
@GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric")
suspend fun forecast(
@Query("lat") lat: Double,
@Query("lon") lon: Double
): Forecast
}Z
class OpenWeatherTemperatureRepository(
private val api: WeatherApi
) : TemperatureRepository {
override suspend fun getTemperature(lat: Double, lon: Double): Temperature =
coroutineScope {
val forecastDeferred = async { api.forecast(lat, lon) }
val weather = api.currentWeather(lat, lon)
val temperatures = forecastDeferred.await().list.map { it.main }
Temperature(
weather.main.temp.toInt(),
temperatures.map { it.temp_min }.min()?.toInt(),
temperatures.map { it.temp_max }.max()?.toInt()
)D
}E
}F
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
location
entities
app
weather
api
domain
location
entities
app
weather
api
domain
location
entities
app
weather
api
domain
location
entities
app
weather
api
domain
location
entities
app
weather
api
domain
location
entities
app
weather
api
domain
location
entities
app
weather
api
domain
location
entities
app
weather
api
repository
entities
UI
data source
domain
domain
entitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
repository
entities
UI
data source
domain
entitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
“source code dependencies
can only point inwards”
“There’s no rule that
says you must always
have just these four”
entitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
Module by feature
OR
Module by layer
entitiesentities
entities
1
domain1
apiRepository
feature1
presenter1
ap1
entities
2
entities
3
featureN
presenterN
…
…
dbRepository
db
ap2
domain2
Prosentitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
Framework
independence
entitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
Architecture
Vs
code conventions
entitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
TDDentitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
Consentitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
More codeentitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
Architecture Vs
code conventions
TDD
Pros
Cons
More code
Framework
independence
entitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
Architecture Vs
code conventions
TDD
Pros
Cons
More code
Framework
independence
domain location
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
Clean Architecture
locationInterfaces
Architecture Vs
code conventions
TDD
Pros
Cons
More code
Framework
independence
domain
location
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
Three modules
Architecture Vs
code conventions
TDD
Pros
Cons
More code
Framework
independence
domain location
class WeatherUseCase(
private val locationManager: LocationManager,
private val repository: TemperatureRepository) {
suspend fun getCityData(): String = try {
coroutineScope {
val location = locationManager.getLastLocation()
val cities = async { locationManager.getCities(location) }
val temperature = repository.getTemperature(
location.lat, location.lon)
val city = cities.await().getOrElse(0) { "No city found" }
"$city n $temperature"
}2
} catch (e: Exception) {
"Error retrieving data: ${e.message}"
}1
}3
interface LocationManager {
suspend fun getLastLocation(): Location
suspend fun getCities(location: Location): List<City>
}6
Gradle api/implementation
class AndroidLocationManager(context: Context) : LocationManager {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val geocoder = Geocoder(context, Locale.getDefault())
override suspend fun getLastLocation(): Location = suspendCoroutine {
//...
}E
override suspend fun getCities(location: Location): List<City> = withContext(IO) {
//...
}E
}E
Architecture Vs
code conventions
TDD
Pros
Cons
More code
Framework
independence
entitiesentitiesentitiesentities
domain
repository
UI
data source
presenter
Links
Demo Project
github.com/fabioCollini/CleanWeather
2000 - Robert C. Martin - Design Principles and Design Patterns
www.cvc.uab.es/shared/teach/a21291/temes/object_oriented_design/
materials_adicionals/principles_and_patterns.pdf
2005 - Robert C. Martin - The Principles of OOD
butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
2012 - Robert C. Martin - The Clean Architecture
8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
THANKS
FOR YOUR
ATTENTION
QUESTIONS?
androiddevs.it

Solid principles in practice the clean architecture - Droidcon Italy

  • 1.
    SOLID principles inpractice: the clean architecture Fabio Collini
  • 2.
  • 3.
  • 6.
    class WeatherUseCase( private vallocationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3
  • 7.
    class WeatherUseCase( private vallocationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 Coroutines
  • 8.
    class WeatherUseCase( private vallocationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3
  • 9.
    class WeatherUseCase( private vallocationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 Dependency Injection
  • 10.
    class WeatherUseCaseTest { vallocationManager: LocationManager = mockk() val repository: TemperatureRepository = mockk() val useCase = WeatherUseCase(locationManager, repository) @Test fun retrieveCityData() { coEvery { locationManager.getLastLocation() } returns LOCATION coEvery { locationManager.getCities(LOCATION) } returns listOf(City("Firenze", "IT")) coEvery { repository.getTemperature(LAT, LON) } returns Temperature(10, 8, 20) val cityData = runBlocking { useCase.getCityData() } assert(cityData).isEqualTo("Firenze (IT) n 10º min 8º max 20º") }1 }2
  • 11.
    class WeatherUseCaseTest { vallocationManager: LocationManager = mockk() val repository: TemperatureRepository = mockk() val useCase = WeatherUseCase(locationManager, repository) @Test fun retrieveCityData() { coEvery { locationManager.getLastLocation() } returns LOCATION coEvery { locationManager.getCities(LOCATION) } returns listOf(City("Firenze", "IT")) coEvery { repository.getTemperature(LAT, LON) } returns Temperature(10, 8, 20) val cityData = runBlocking { useCase.getCityData() } assert(cityData).isEqualTo("Firenze (IT) n 10º min 8º max 20º") }1 }2
  • 12.
    class WeatherUseCaseTest { vallocationManager: LocationManager = mockk() val repository: TemperatureRepository = mockk() val useCase = WeatherUseCase(locationManager, repository) @Test fun retrieveCityData() { coEvery { locationManager.getLastLocation() } returns LOCATION coEvery { locationManager.getCities(LOCATION) } returns listOf(City("Firenze", "IT")) coEvery { repository.getTemperature(LAT, LON) } returns Temperature(10, 8, 20) val cityData = runBlocking { useCase.getCityData() } assert(cityData).isEqualTo("Firenze (IT) n 10º min 8º max 20º") }1 }2
  • 13.
    class WeatherUseCaseTest { vallocationManager: LocationManager = mockk() val repository: TemperatureRepository = mockk() val useCase = WeatherUseCase(locationManager, repository) @Test fun retrieveCityData() { coEvery { locationManager.getLastLocation() } returns LOCATION coEvery { locationManager.getCities(LOCATION) } returns listOf(City("Firenze", "IT")) coEvery { repository.getTemperature(LAT, LON) } returns Temperature(10, 8, 20) val cityData = runBlocking { useCase.getCityData() } assert(cityData).isEqualTo("Firenze (IT) n 10º min 8º max 20º") }1 }2
  • 14.
    class WeatherUseCase( private vallocationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3
  • 15.
    interface LocationManager { suspendfun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 interface TemperatureRepository { suspend fun getTemperature(lat: Double, lon: Double): Temperature }7
  • 16.
    interface LocationManager { suspendfun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 interface TemperatureRepository { suspend fun getTemperature(lat: Double, lon: Double): Temperature }7 class OpenWeatherTemperatureRepository( private val api: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 17.
    class AndroidLocationManager(context: Context): LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 18.
    class AndroidLocationManager(context: Context): LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) @SuppressLint("MissingPermission") override suspend fun getLastLocation(): Location = suspendCoroutine { continuation -> fusedLocationClient.lastLocation .addOnSuccessListener { location -> if (location == null) continuation.resumeWithException(Exception("Location not available")) else continuation.resume(Location(location.latitude, location.longitude)) } .addOnFailureListener { continuation.resumeWithException(it) } }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { geocoder.getFromLocation(location.lat, location.lon, 10) .filter { it.locality != null } .map { City(it.locality, it.countryCode) } }E }E
  • 19.
    interface LocationManager { suspendfun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 interface TemperatureRepository { suspend fun getTemperature(lat: Double, lon: Double): Temperature }7 class OpenWeatherTemperatureRepository( private val api: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 20.
    class OpenWeatherTemperatureRepository( private valapi: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F
  • 21.
    class OpenWeatherTemperatureRepository( private valapi: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F
  • 22.
    interface LocationManager { suspendfun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 interface TemperatureRepository { suspend fun getTemperature(lat: Double, lon: Double): Temperature }7 class OpenWeatherTemperatureRepository( private val api: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E interface WeatherApi { @GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun currentWeather( @Query("lat") lat: Double, @Query("lon") lon: Double ): TemperatureWrapper @GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun forecast( @Query("lat") lat: Double, @Query("lon") lon: Double ): Forecast }Z
  • 23.
    interface WeatherApi { @GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric") suspendfun currentWeather( @Query("lat") lat: Double, @Query("lon") lon: Double ): TemperatureWrapper @GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun forecast( @Query("lat") lat: Double, @Query("lon") lon: Double ): Forecast }Z
  • 24.
    class WeatherViewModel(app: Application): AndroidViewModel(app) { private val api = RetrofitFactory.createService<WeatherApi>() private val weatherRepository = OpenWeatherTemperatureRepository(api) private val positionManager = AndroidLocationManager(app) private val useCase = WeatherUseCase(positionManager, weatherRepository) val state = MutableLiveData<String>() fun load() { viewModelScope.launch { val result = useCase.getCityData() state.value = result }O }O }O class MainActivity : AppCompatActivity() { //... } interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 interface TemperatureRepository { suspend fun getTemperature(lat: Double, lon: Double): Temperature }7 interface WeatherApi { @GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun currentWeather( @Query("lat") lat: Double, @Query("lon") lon: Double ): TemperatureWrapper @GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun forecast( @Query("lat") lat: Double, @Query("lon") lon: Double ): Forecast }Z class OpenWeatherTemperatureRepository( private val api: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 25.
    class WeatherViewModel(app: Application): AndroidViewModel(app) { private val api = RetrofitFactory.createService<WeatherApi>() private val weatherRepository = OpenWeatherTemperatureRepository(api) private val positionManager = AndroidLocationManager(app) private val useCase = WeatherUseCase(positionManager, weatherRepository) val state = MutableLiveData<String>() fun load() { viewModelScope.launch { val result = useCase.getCityData() state.value = result }O }O }O
  • 26.
    app api domain weather location class MainActivity :AppCompatActivity() { //... } interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 interface TemperatureRepository { suspend fun getTemperature(lat: Double, lon: Double): Temperature }7 class WeatherViewModel(app: Application) : AndroidViewModel(app) { private val api = RetrofitFactory.createService<WeatherApi>() private val weatherRepository = OpenWeatherTemperatureRepository(api) private val positionManager = AndroidLocationManager(app) private val useCase = WeatherUseCase(positionManager, weatherRepository) val state = MutableLiveData<String>() fun load() { viewModelScope.launch { val result = useCase.getCityData() state.value = result }O }O }O interface WeatherApi { @GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun currentWeather( @Query("lat") lat: Double, @Query("lon") lon: Double ): TemperatureWrapper @GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun forecast( @Query("lat") lat: Double, @Query("lon") lon: Double ): Forecast }Z class OpenWeatherTemperatureRepository( private val api: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 27.
    Java/Kotlin modules aremore… Testable Reusable
  • 28.
    Robert C. MartinCopyright (c) 2000 by Robert C. Martin. All Rights Reserved. www.objectmentor.com 1 Design Principles and Design Patterns Robert C. Martin www.objectmentor.com What is software architecture? The answer is multitiered. At the highest level, there are the architecture patterns that define the overall shape and structure of software applications1 . Down a level is the architecture that is specifically related to the pur- pose of the software application. Yet another level down resides the architecture of the modules and their interconnections. This is the domain of design patterns2 , pack- akges, components, and classes. It is this level that we will concern ourselves with in this chapter. Our scope in this chapter is quite limitted. There is much more to be said about the principles and patterns that are exposed here. Interested readers are referred to [Martin99]. Architecture and Dependencies What goes wrong with software? The design of many software applications begins as a vital image in the minds of its designers. At this stage it is clean, elegant, and com- pelling. It has a simple beauty that makes the designers and implementers itch to see it working. Some of these applications manage to maintain this purity of design through the initial development and into the first release. But then something begins to happen. The software starts to rot. At first it isn’t so bad. An ugly wart here, a clumsy hack there, but the beauty of the design still shows through. Yet, over time as the rotting continues, the ugly festering sores and boils accumulate until they dominate the design of the application. The program becomes a festering mass of code that the developers find increasingly hard to maintain. Eventu- 1. [Shaw96] 2. [GOF96]
  • 29.
    Copyright (c) 2000by Robert C. Martin. All Rights Reserved.
  • 30.
  • 31.
    Single responsibility “A classshould have one, and only one, reason to change”
  • 32.
    app domain weather interface TemperatureRepository { suspendfun getTemperature(lat: Double, lon: Double): Temperature }7 class WeatherViewModel(app: Application) : AndroidViewModel(app) { private val api = RetrofitFactory.createService<WeatherApi>() private val weatherRepository = OpenWeatherTemperatureRepository(api) private val positionManager = AndroidLocationManager(app) private val useCase = WeatherUseCase(positionManager, weatherRepository) val state = MutableLiveData<String>() fun load() { viewModelScope.launch { val result = useCase.getCityData() state.value = result }O }O }O class MainActivity : AppCompatActivity() { //... } location interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 api interface WeatherApi { @GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun currentWeather( @Query("lat") lat: Double, @Query("lon") lon: Double ): TemperatureWrapper @GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun forecast( @Query("lat") lat: Double, @Query("lon") lon: Double ): Forecast }Z class OpenWeatherTemperatureRepository( private val api: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 33.
  • 34.
    Open closed “You shouldbe able to extend a classes behavior, without modifying it”
  • 35.
  • 36.
  • 37.
  • 38.
    Liskov substitution “Derived classesmust be substitutable for their base classes”
  • 39.
    open class Rectangle( openvar width: Int, open var height: Int ) { fun area() = width * height } class Square(size: Int) : Rectangle(size, size) { override var width: Int = size set(value) { field = value if (height != value) height = value } override var height: Int = size set(value) { field = value if (width != value) width = value } }
  • 40.
    var rectangle =Rectangle(6, 4) var initialArea = rectangle.area() rectangle = Square(3) initialArea = rectangle.area() rectangle.height *= 2 println(initialArea * 2 == rectangle.area()) rectangle.height *= 2 println(initialArea * 2 == rectangle.area())
  • 41.
  • 42.
    Interface segregation “Make finegrained interfaces that are client specific”
  • 43.
  • 44.
  • 45.
  • 46.
    Dependency inversion “Depend onabstractions, not on concretions”
  • 47.
    app domain weather interface TemperatureRepository { suspendfun getTemperature(lat: Double, lon: Double): Temperature }7 class WeatherViewModel(app: Application) : AndroidViewModel(app) { private val api = RetrofitFactory.createService<WeatherApi>() private val weatherRepository = OpenWeatherTemperatureRepository(api) private val positionManager = AndroidLocationManager(app) private val useCase = WeatherUseCase(positionManager, weatherRepository) val state = MutableLiveData<String>() fun load() { viewModelScope.launch { val result = useCase.getCityData() state.value = result }O }O }O class MainActivity : AppCompatActivity() { //... } location interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 api interface WeatherApi { @GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun currentWeather( @Query("lat") lat: Double, @Query("lon") lon: Double ): TemperatureWrapper @GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun forecast( @Query("lat") lat: Double, @Query("lon") lon: Double ): Forecast }Z class OpenWeatherTemperatureRepository( private val api: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 48.
    app domain weather interface TemperatureRepository { suspendfun getTemperature(lat: Double, lon: Double): Temperature }7 class WeatherViewModel(app: Application) : AndroidViewModel(app) { private val api = RetrofitFactory.createService<WeatherApi>() private val weatherRepository = OpenWeatherTemperatureRepository(api) private val positionManager = AndroidLocationManager(app) private val useCase = WeatherUseCase(positionManager, weatherRepository) val state = MutableLiveData<String>() fun load() { viewModelScope.launch { val result = useCase.getCityData() state.value = result }O }O }O class MainActivity : AppCompatActivity() { //... } location interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 api interface WeatherApi { @GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun currentWeather( @Query("lat") lat: Double, @Query("lon") lon: Double ): TemperatureWrapper @GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun forecast( @Query("lat") lat: Double, @Query("lon") lon: Double ): Forecast }Z class OpenWeatherTemperatureRepository( private val api: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 49.
    Dependency inversion “Depend onabstractions, not on concretions”
  • 50.
    Dependency inversion “High levelmodules should not depend upon low level modules”
  • 51.
    Dependency inversion “High levelmodules should not depend upon low level modules”
  • 52.
    app domain weather interface TemperatureRepository { suspendfun getTemperature(lat: Double, lon: Double): Temperature }7 class WeatherViewModel(app: Application) : AndroidViewModel(app) { private val api = RetrofitFactory.createService<WeatherApi>() private val weatherRepository = OpenWeatherTemperatureRepository(api) private val positionManager = AndroidLocationManager(app) private val useCase = WeatherUseCase(positionManager, weatherRepository) val state = MutableLiveData<String>() fun load() { viewModelScope.launch { val result = useCase.getCityData() state.value = result }O }O }O class MainActivity : AppCompatActivity() { //... } location interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 api interface WeatherApi { @GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun currentWeather( @Query("lat") lat: Double, @Query("lon") lon: Double ): TemperatureWrapper @GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun forecast( @Query("lat") lat: Double, @Query("lon") lon: Double ): Forecast }Z class OpenWeatherTemperatureRepository( private val api: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 53.
    domain location interface LocationManager{ suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3
  • 54.
    domain location class WeatherUseCase( privateval locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 55.
    app domain weather interface TemperatureRepository { suspendfun getTemperature(lat: Double, lon: Double): Temperature }7 location interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E class WeatherViewModel(app: Application) : AndroidViewModel(app) { private val api = RetrofitFactory.createService<WeatherApi>() private val weatherRepository = OpenWeatherTemperatureRepository(api) private val positionManager = AndroidLocationManager(app) private val useCase = WeatherUseCase(positionManager, weatherRepository) val state = MutableLiveData<String>() fun load() { viewModelScope.launch { val result = useCase.getCityData() state.value = result }O }O }O class MainActivity : AppCompatActivity() { //... } api interface WeatherApi { @GET("weather?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun currentWeather( @Query("lat") lat: Double, @Query("lon") lon: Double ): TemperatureWrapper @GET("forecast?appid=$OPEN_WEATHER_APP_ID&units=metric") suspend fun forecast( @Query("lat") lat: Double, @Query("lon") lon: Double ): Forecast }Z class OpenWeatherTemperatureRepository( private val api: WeatherApi ) : TemperatureRepository { override suspend fun getTemperature(lat: Double, lon: Double): Temperature = coroutineScope { val forecastDeferred = async { api.forecast(lat, lon) } val weather = api.currentWeather(lat, lon) val temperatures = forecastDeferred.await().list.map { it.main } Temperature( weather.main.temp.toInt(), temperatures.map { it.temp_min }.min()?.toInt(), temperatures.map { it.temp_max }.max()?.toInt() )D }E }F class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
    entitiesentitiesentitiesentities domain repository UI data source presenter “source codedependencies can only point inwards” “There’s no rule that says you must always have just these four”
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 70.
  • 71.
  • 72.
  • 73.
    Architecture Vs code conventions TDD Pros Cons Morecode Framework independence entitiesentitiesentitiesentities domain repository UI data source presenter
  • 74.
    Architecture Vs code conventions TDD Pros Cons Morecode Framework independence domain location class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E Clean Architecture
  • 75.
    locationInterfaces Architecture Vs code conventions TDD Pros Cons Morecode Framework independence domain location class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E Three modules
  • 76.
    Architecture Vs code conventions TDD Pros Cons Morecode Framework independence domain location class WeatherUseCase( private val locationManager: LocationManager, private val repository: TemperatureRepository) { suspend fun getCityData(): String = try { coroutineScope { val location = locationManager.getLastLocation() val cities = async { locationManager.getCities(location) } val temperature = repository.getTemperature( location.lat, location.lon) val city = cities.await().getOrElse(0) { "No city found" } "$city n $temperature" }2 } catch (e: Exception) { "Error retrieving data: ${e.message}" }1 }3 interface LocationManager { suspend fun getLastLocation(): Location suspend fun getCities(location: Location): List<City> }6 Gradle api/implementation class AndroidLocationManager(context: Context) : LocationManager { private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val geocoder = Geocoder(context, Locale.getDefault()) override suspend fun getLastLocation(): Location = suspendCoroutine { //... }E override suspend fun getCities(location: Location): List<City> = withContext(IO) { //... }E }E
  • 77.
    Architecture Vs code conventions TDD Pros Cons Morecode Framework independence entitiesentitiesentitiesentities domain repository UI data source presenter
  • 78.
    Links Demo Project github.com/fabioCollini/CleanWeather 2000 -Robert C. Martin - Design Principles and Design Patterns www.cvc.uab.es/shared/teach/a21291/temes/object_oriented_design/ materials_adicionals/principles_and_patterns.pdf 2005 - Robert C. Martin - The Principles of OOD butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod 2012 - Robert C. Martin - The Clean Architecture 8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
  • 79.
  • 80.