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

Clean Architecture con Kotlin y Rx: otro enfoque

Clean Architecture con Kotlin y Rx: otro enfoque

Esta charla la presenté en un Webinar de Codemotion el 14 de noviembre de 2018. En ella presenté los aspectos básicos que utilizo en todos los proyectos de Android en los que participo. La idea de la presentación era transmitir un nuevo enfoque para aplicar Clean Architecture con todas las ventajas de Kotlin y Rx, añadiendo una visión más al amplio espectro de arquitecturas ya existente.

Adrián Santalla

November 14, 2018
Tweet

Other Decks in Programming

Transcript

  1. Adrián Santalla @ ninety nine Senior Android Engineer @ www.ninetynine.com

    Contacto: Twitter: @asantalla GitHub: https://github.com/asantalla Email: [email protected]
  2. ! Un aplicación con una arquitectura sólida debe ser: !

    Escalable y mantenible. ! Fácilmente testeable. ! Cada componente debe estar aislado y desacoplado. ! Independiente de frameworks? Introducción
  3. ! Independiente de la frameworks? ! En este caso no

    se cumple ;) ! RxJava + Kotlin + Android encaja muy bien para conseguir una arquitectura Clean. ! El principal problema que podemos encontrar es la curva de aprendizaje de RxJava. Introducción +
  4. ! Kotlin es un lenguaje: ! Conciso y permite crear

    código más limpio. ! Permite generar clases inmutables y controlar la nullibilidad. ! Es interoperable con cualquier lenguaje de la JVM. ! Está en constante evolución, actualmente la versión 1.3. ! Está aceptado oficialmente por Google. ! La comunidad de desarrolladores es cada vez más grande. Introducción
  5. ! RxJava: ! Es una implementación de programación reactiva. !

    Se basa en el patrón Observer (Productor/Consumidor). ! Favorece la composición mediante funciones evitando estados globales y side effects. ! Nos permite programar orientado a eventos asíncronos. ! Think Reactive. Introducción
  6. ! Conceptos básicos de RxJava: ! Observable ! Observer !

    Subject ! Completable ! … Introducción
  7. Estructura del proyecto App Domain Data ! Use Cases !

    Resources & Repositories ! API Definition ! Domain Model ! DB & API clients ! Providers ! DB & API Model ! Dependency Injection ! Views ! MVP ! UI Model
  8. Clients ! Es dependiente de la plataforma. ! Provee los

    datos de bases de datos, servidores de APIs, Sockets, … ! Utilizamos librerías que soporten RxJava. ! Retrofit: conexiones con APIs. ! Room: base de datos.
  9. Clients - Retrofit interface OAuthApiDefinition { @POST("token") fun login(@Body loginRequest:

    LoginRequest): Observable<AuthResponse> @POST("token") fun refreshToken(@Body refreshTokenRequest: RefreshTokenRequest): Observable<AuthResponse> } class AuthApiClient( private val oAuthApiDefinition: OAuthApiDefinition, private val clientId: String, private val clientSecret: String ) : AuthApi { override fun login(email: String, password: String): Observable<AuthData> = oAuthApiDefinition.login( LoginRequest(…) ).map { it.toDomain() } override fun refreshToken(refreshToken: String): Observable<AuthData> = oAuthApiDefinition.refreshToken( RefreshTokenRequest(…) ).map { it.toDomain() } } Definición del API de Retrofit El API client implementa la definición del API del módulo de Domain Convertimos el modelo de API al modelo de dominio utilizando operadores de RxJava
  10. Repository ! Son los encargados de proveer los datos como

    modelos de nuestro dominio. ! Un Repositorio se configura con hasta 3 tipos de DataSource. ! CacheableDataSource: en memoria. ! ReadableDataSource: suele ser un API client o una base de datos. ! WriteableDataSource: suele ser una base de datos. ! Podemos crearnos un BaseRepository genérico que nos permita cualquier combinación gracias a los operadores de RxJava.
  11. interface ReadableDataSource<K, V : Identifiable<K>> : DataSource { fun getByKey(key:

    K): Observable<V> fun getAll(): Observable<List<V>> } interface WriteableDataSource<K, V : Identifiable<K>> : DataSource { fun addOrUpdate(value: V): Observable<V> fun replaceAll(values: List<V>): Observable<List<V>> fun deleteByKey(key: K): Completable fun deleteAll(): Completable } interface CacheableDataSource<K, V : Identifiable<K>> : ReadableDataSource<K, V>, WriteableDataSource<K, V> { val policies: List<CachePolicy> fun isValid(value: V): Boolean } interface Identifiable<K> { val key: K } Representa un objeto que se puede identificar únicamente con su key Repository
  12. class BaseRepository<K, V : Identifiable<K>>( private val cacheableDataSource: CacheableDataSource<K, V>?

    = null, private val writeableDataSource: WriteableDataSource<K, V>? = null, private val readableDataSource: ReadableDataSource<K, V>? = null ) : ReadableDataSource<K, V>, WriteableDataSource<K, V> { fun getByKey(key: K): Observable<V> = Observable.concat( cacheableDataSource?.getByKey(key).switchIfNullToEmpty(), readableDataSource?.getByKey(key).switchIfNullToEmpty() .flatMap { readableValue -> cacheableDataSource?.addOrUpdate(readableValue) .switchIfNullToEmpty() .switchIfEmpty(Observable.just(readableValue)) } ).firstElement() .toObservable() … } private fun Observable<V>?.switchIfNullToEmpty(): Observable<V> = this ?: Observable.empty() Repository Aprovechamos la potencia de RxJava y mediante sus operadores combinamos Observables para obtener y almacenar los datos
  13. Resource ! El Resource nace como necesidad de crear una

    capa que cumpla con las siguientes expectativas: ! Poder acceder directamente a una instancia del API sin necesidad de montar un repositorio. ! Es complicado crear un DataSource genérico que cumpla con cualquier contrato de API. En especial las llamadas parametrizadas.
  14. class AuthResource( private val authApi: AuthApi, private val authRepository: BaseRepository<String,

    AuthData> ) { fun login(email: String, password: String): Observable<AuthData> = authApi.login( email = email, password = password ).flatMap { authRepository.addOrUpdate(it) } } Resource
  15. Use Case ! Un Use Case representa una lógica de

    negocio. ! Al menos utiliza un Resource para obtener los datos. ! En el Use Case se realiza el cambio de hilo para que el procesamiento no se ejecute en el hilo principal.
  16. interface LoginUseCase { fun execute(email: String, password: String): Observable<AuthData> }

    class Login( private val authResource: AuthResource ) : LoginUseCase { override fun execute(email: String, password: String): Observable<AuthData> = authResource.login( email = email, password = password ).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) } Use Case
  17. MVP ! El Presenter es el encargado de manejar a

    la vista, ya sea un Activity o un Fragment. ! La vista, implementa una interfaz BaseView para que el Presenter no tenga conocimiento del tipo de vista. ! El Navigator es la entidad encargada de realizar la navegaciones entre pantallas. Se inyecta en el Presenter, y éste se encarga de navegar. ! La vista, solo se encarga de pintar la pantalla.
  18. abstract class Presenter<V : BaseView> { private var view: WeakReference<V>?

    = null private var compositeDisposable: CompositeDisposable? = null fun setView(view: V) { this.view = WeakReference(view) this.compositeDisposable = CompositeDisposable() } fun getView(): V? = view?.get() fun addSubscription(disposable: Disposable) { compositeDisposable?.add(disposable) } abstract fun onCreate() abstract fun onResume() abstract fun onPause() fun onDestroy() { view = null compositeDisposable?.dispose() } } MVP - Presenter
  19. class ActivityNavigator( private val activity: AppCompatActivity, private val viewStateSaver: ViewStateSaver

    ) : ActivityNavigatorDefinition { override fun goToLoginScreen() { activity.goToActivity( activityClass = LoginActivity::class.java, flags = FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_NO_ANIMATION ) } … } MVP - Navigator Un Navigator se instancia por Activity (o Fragment, si tuviera uno), por lo que se utiliza el Context correcto ViewStateSaver????
  20. interface ViewStateSaver { fun save(view: KClass<out BaseView>, viewState: ViewState) }

    interface ViewStateLoader { fun <STATE> load(view: KClass<out BaseView>): STATE? where STATE : ViewState? } open class ViewStateStore : ViewStateSaver, ViewStateLoader { private val states: MutableMap<KClass<out BaseView>, Stack<ViewState>> = HashMap() override fun save(view: KClass<out BaseView>, viewState: ViewState) { if (states[view] == null) { states[view] = Stack() } states[view]!!.push(viewState) } @kotlin.Suppress("UNCHECKED_CAST") override fun <STATE> load(view: KClass<out BaseView>): STATE? where STATE : ViewState? { val viewStateStack: Stack<ViewState>? = states[view] return viewStateStack?.let { stack: Stack<ViewState> -> val state: STATE? = stack.pop() as STATE? if (stack.isEmpty()) { states.remove(view) } return state } } } MVP - ViewStateSaver Surge por la necesidad de desacoplar el traspaso de datos entre pantalla del ciclo natural de Android Intents: No queremos que la vista sepa cómo enviar datos a otras pantallas, para eso tenemos al Navigator
  21. ! Independiente de la plataforma. ! Utilizamos Android para pintar

    las vistas y para inicializar los clientes. ! Facilmente escalable y mantenible. ! Testeable por componentes. Conclusión
  22. ! Kotlin: https://kotlinlang.org/ ! Observer Pattern: https://sourcemaking.com/design_patterns/observer ! ReactiveX: http://reactivex.io/

    ! RxJava: https://github.com/ReactiveX/RxJava ! Reactive Programming with RxJava [Libro]: http://shop.oreilly.com/ product/0636920042228.do Enlaces