Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Clean architecture and white label

Avatar for João Reis João Reis
January 26, 2019

Clean architecture and white label

Building white label apps can be a daunting task, especially because requirements and design will most likely change from client to client. At ebankit we address this challenge by taking yet another approach to the good old clean architecture.
This talk dived into the concepts of Clean Architecture applied to Android with a modular and semi-functional approach by leveraging the features of the Kotlin language and Kotlin co-routines.

Avatar for João Reis

João Reis

January 26, 2019

Other Decks in Technology

Transcript

  1. Clean architecture and white label A case study João Reis,

    Senior Android Developer @ ebankIT GDG DEVFEST PORTO, JAN 2019
  2. The challenge • White label banking application • Requirements and

    design change from client to client • Most of the business logic is common to all clients and should be reused when possible • Should be able to accommodate custom services and business logic • New features are added regularly 4
  3. The solution • Modular • Adding new features or customizing

    existent ones should be easy and quick • Common business logic should be isolated • Flexible • Testable 5
  4. The solution • Modular • Adding new features or customizing

    existent ones should be easy and quick • Common business logic should be isolated • Testable • Flexible 6 A good fit for clean architecture principles.
  5. Yet another way to look at it 13 Presentation layer

    Abstraction Abstraction Abstraction Use case interactor Data models Business logic Data Web API Android Framework DB Cache
  6. 18 Data Source Web API Android Framework DB Cache interface

    Callback { fun onError(val error: Error) fun onSuccess(val data: DataObject) }
  7. 20 Data Source Web API Android Framework DB Cache sealed

    class Result<out S, out E> { data class Success<S>(val data: S) : Result<S, Nothing>() data class Error<E>(val error: E) : Result<Nothing, E>() }
  8. 21 Use case interactor Data source Data source DataResponse DataResponse

    UseCaseResult sealed class DataResponse<out T> { data class Success<T>(val data: T) : DataResponse <T>() data class Error(val error: ErrorObj) : DataResponse <Nothing>() } sealed class UseCaseResult<out T> { data class Success<T>(val data: T) : UseCaseResult<T>() data class Error(val error: ErrorObj) : UseCaseResult<Nothing>() }
  9. Suspending functions • Functions that can be paused and resumed

    • Continuation Passing Style 30 fun doSomething(item: Item) { CoroutineScope(Dispatchers.Default).launch { val result = longRunningIOCall(item) val processedResult = longRunningComputation(result) withContext(Dispatchers.Main) { showResult(processedResult) } } } suspend fun longRunningNetworkCall(input: Item) : SomeType {...} suspend fun longRunningComputation(input: SomeType) : SomeOtherType {...}
  10. Suspending functions • Functions that can be paused and resumed

    • Continuation Passing Style 31 public interface Continuation<in T> { public val context: CoroutineContext public fun resumeWith(result: Result<T>) } fun longRunningNetworkCall(input: Item, callback: Continuation<SomeType>): SomeType {...} fun doSomething(item: Item) { CoroutineScope(Dispatchers.Default).launch { val result = longRunningIOCall(item) val processedResult = longRunningComputation(result) withContext(Dispatchers.Main) { showResult(processedResult) } } } suspend fun longRunningNetworkCall(input: Item) : SomeType {...} suspend fun longRunningComputation(input: SomeType) : SomeOtherType {...}
  11. Coroutine builders 32 fun doSomething(item: Item) { CoroutineScope(Dispatchers.Default).launch { val

    result = longRunningIOCall(item) val processedResult = longRunningComputation(result) withContext(Dispatchers.Main) { showResult(processedResult) } } } suspend fun longRunningNetworkCall(input: Item) : SomeType {...} suspend fun longRunningComputation(input: SomeType) : SomeOtherType {...} launch {} async {} runBlocking {} Returns Job object Returns Deferred object Blocks the current thread until the execution of the coroutine
  12. Dispatchers 33 • Dispatchers.Default • Dispatchers.IO • Dispatchers.Main fun doSomething(item:

    Item) { CoroutineScope(Dispatchers.Default).launch { val result = longRunningIOCall(item) val processedResult = longRunningComputation(result) withContext(Dispatchers.Main) { showResult(processedResult) } } } suspend fun longRunningNetworkCall(input: Item) : SomeType {...} suspend fun longRunningComputation(input: SomeType) : SomeOtherType {...}
  13. Structured Concurrency • Scopes have a parent-child relationship • Parent

    scope cancellation cancels children 34 fun doStuffInParallel() { val parentJob = CoroutineScope(Dispatchers.Default).launch { val someData = async { foo() } val someOtherData = async { bar() } whatever(someData.await(), someOtherData.await()) } }
  14. 35 open class BaseViewModel : ViewModel(), CoroutineScope { override val

    coroutineContext: CoroutineContext get() = Dispatchers.Default + Job() protected inline fun doOnBackground(crossinline block: suspend () -> Unit) { launch { block() } } override fun onCleared() { super.onCleared() coroutineContext.cancel() } }
  15. The interactor 37 abstract class UseCaseInteractor<I, O> { protected abstract

    suspend fun validateInput(input: I): Boolean protected abstract suspend fun run(input: I): UseCaseResult<O> suspend operator fun invoke(input: I) = validateInput(input).let { if (it) { run(input) } else { UseCaseResult.Error(ErrorFactory.invalidInputError()) } } } I Validate Input Execute logic Is Valid ? UseCaseInteractor<I, O> Y N UseCaseResult.Error UseCaseResult<O> UseCaseResult<O>
  16. Data sources 38 Remote data Local data Implements CustomerServiceImpl <<CustomerService>>

    EbankitService Extends Implements UserDataStoreImpl <<UserDataStore>>
  17. Ebankit service 39 open class EbankitService<S>(private val retrofit: Retrofit, private

    val coreConfig: CoreConfigurations, private val httpClient: OkHttpClient, private val appContext: Context, serviceClass: Class<S>) val apiImplementation: S = retrofit.create(serviceClass) suspend inline fun <R, T : ResponseObject<R>> executeRequest(useCache: Boolean = false, extraHeaders: HashMap<String, String> = HashMap(), crossinline method: S.(HashMap<String, String>) -> Deferred<Response<T>>): DataResponse<T> = withContext(Dispatchers.IO) { extraHeaders.combineHeaders(useCache) apiImplementation.method(extraHeaders).awaitAndMap() } suspend fun <R, T : ResponseObject<R>> Deferred<Response<T>>.awaitAndMap(): DataResponse<T> { return try { return this.await().mapData() } catch (e: Exception) { DataResponse.Error(ErrorFactory.genericServiceError()) } }
  18. Ebankit service 40 interface CustomerAPI { @POST(Endpoints.ServiceProducts) fun getProducts(@HeaderMap headerMap:

    Map<String, String>, @Body requestCustomerProducts: RequestCustomerProducts): Deferred<Response<ResponseCustomerProducts>> } class CustomerServiceImpl(retrofit: Retrofit, coreConfig: CoreConfigurations, httpClient: OkHttpClient, appContext: Context) : EbankitService<CustomerAPI>(retrofit, coreConfig, httpClient, appContext, CustomerAPI::class.java), CustomerService { override suspend fun getProductsList(productTypes: ArrayList<Int>, extendedProperties: List<ExtendedPropertie>, extraHeaders: HashMap<String, String>, useCache: Boolean): DataResponse<ResponseCustomerProducts> { val requestCustomerProducts = RequestCustomerProducts(extendedProperties, productTypes) return executeRequest(useCache, extraHeaders) { getProducts(it, requestCustomerProducts) } } }
  19. List all the products the current user owns 41 •

    Fetch customer products from API • Fetch user data from local storage • Filter the products • Display the results
  20. 42 Presentation Domain Data Access Web API DB ProductListViewModel CustomerServiceImpl

    <<CustomerService>> UserDataStoreImpl <<UserDataStore>> GetProducts ProductListView
  21. The interactor 43 class GetProducts(val customerService: CustomerService, val userDataStore :

    UserDataStore) : UseCaseInteractor<ProductsInput, List<CustomerProduct>>() { override suspend fun run(input: ProductsInput): UseCaseResult<List<CustomerProduct>> { val productsResult = customerService.getProductsList(input.productsTypes, input.extendedProperties, useCache = true) return when(productsResult) { is DataResponse.Success -> { val loginAccounts = userDataStore.getLoginAccounts() when(loginAccounts) { is DataResponse.Success -> UseCaseResult.Success(filterProducts(productsResult.data, input, loginAccounts.data)) is DataResponse.Error -> UseCaseResult.Error(loginAccounts.error) } } is DataResponse.Error -> UseCaseResult.Error(productsResult.error) } } }
  22. The View Model 44 class ProductsListViewModel(val getProducts: GetProducts) : BaseViewModel()

    { val productListViewState = MutableLiveData<ProductsViewState>() fun loadAllProducts() { doOnBackground { productListViewState.postValue(ProductsViewState.Loading) val productsInput = ProductsInput(...) val result = getProducts(productsInput) .map { when (it) { is UseCaseResult.Success -> ProductsViewState.Success(it.data) is UseCaseResult.Error -> ProductsViewState.Error(it.error) } } productListViewState.postValue(result) } } } sealed class ProductsViewState { object Loading : ProductsViewState() class Error(val error: ErrorObj) : ProductsViewState() class Success(val productList: List<CustomerProduct>) : ProductsViewState() }
  23. The view viewModel.productListViewState.nonNullObserve(this) { when (it) { is ProductListViewState.Loading ->

    showLoading() is ProductListViewState.Error -> showError(it.error) is ProductListViewState.Success -> showData(it.productList) } }
  24. 46 class GetProducts(val customerService: CustomerService, val userDataStore : UserDataStore) :

    UseCaseInteractor<ProductsInput, List<CustomerProduct>>() { override suspend fun validateInput(input: ProductsInput): Boolean { return true } override suspend fun run(input: ProductsInput): UseCaseResult<List<CustomerProduct>> { val productsResult = customerService.getProductsList(input.productsTypes, input.extendedProperties, useCache = true) return when(productsResult) { is DataResponse.Success -> { val loginAccounts = userDataStore.getLoginAccounts() when(loginAccounts) { is DataResponse.Success -> UseCaseResult.Success(filterProducts(productsResult.data, input, loginAccounts.data)) is DataResponse.Error -> UseCaseResult.Error(loginAccounts.error) } } is DataResponse.Error -> UseCaseResult.Error(productsResult.error) } } } This kinda sucks :/
  25. 47 inline fun <R, T> DataResponse<T>.then(block: (T) -> UseCaseResult<R>): UseCaseResult<R>

    { return when(this) { is DataResponse.Success -> block(this.data) is DataResponse.Error -> UseCaseResult.Error(this.error) } } We can get a bit more functional
  26. 48 inline fun <R, T> DataResponse<T>.then(block: (T) -> UseCaseResult<R>): UseCaseResult<R>

    { return when(this) { is DataResponse.Success -> block(this.data) is DataResponse.Error -> UseCaseResult.Error(this.error) } } return customerService.getProductsList(input.productsTypes, input.extendedProperties, useCache = true) .then { products -> userDataStore.getLoginAccounts().then { UseCaseResult.Success(filterProducts(products, input, it)) } } We can get a bit more functional
  27. 49 We can get a bit more functional doOnBackground {

    val result = interactorA(inputA).then { interactorB(inputB) } } inline fun <T, R> UseCaseResult<T>.then(block: (UseCaseResult.Success<T>) -> UseCaseResult<R>): UseCaseResult<R> { return when(this) { is UseCaseResult.Error -> this is UseCaseResult.Success -> block(this) } }
  28. Thank you [email protected] | www.ebankit.com +44 20 32 87 65

    92 +351 22 203 20 10 Level39, One Canada Square, Canary Wharf E14 5AB, London - United Kingdom Rua José Falcão 133 4050-317, Porto – Portugal