DEV Community

Cover image for Melhorando a arquitetura da Pokédex
Ronaldo Costa de Freitas
Ronaldo Costa de Freitas

Posted on • Edited on

Melhorando a arquitetura da Pokédex

Nesse post nós vamos usar o Koin para delegar a injeção das dependências da nossa aplicação, como Retrofit, API service, repositórios e ViewModels e implementar a arquitetura limpa no nosso projeto com interfaces e use cases.

Clean Architecture

A Clean Architecture (Arquitetura Limpa) é, como o próprio nome diz, uma arquitetura de desenvolvimento de software que visa focar no domínio da aplicação; sendo os drivers, frameworks e libraries apenas detalhes da aplicação. O objetivo é o principio da responsabilidade única, separando o interesse de cada módulo e mantendo as regras de negócio sem conhecer qualquer detalhe sobre o mundo exterior; assim, eles podem ser testados sem dependência de qualquer elemento externo.

Clean Architecture

De forma resumida, o nosso sistema será dividido em três camadas:

  • Presentation (módulo Android): responsável pela interface do aplicativo e a exibição dos dados recebidos do domínio.
  • Domain (módulo Kotlin): responsável pelas entidades e as regras de domínio específicas do projeto. Esse módulo deve ser totalmente independente da plataforma Android.
  • Infrastructure (módulo Android): responsável pelo banco de dados, acesso a internet e outros “detalhes” da aplicação.

Na camada presentation temos as Activities e Fragments, não deve haver lógica dentro delas que não seja a lógica de UI.

Por outro lado, na camada domain estão as entidades, interfaces dos repositories e use cases, é aqui que fica nossa lógica de negócios e serve como ponte entre a camada presentation e a infrastructure.

Por fim, na camada infrastructure estão os dados necessários para a nossa aplicação (chamadas à uma API Rest no nosso caso) que são acessados a partir dos repositories definidos na camada domain.

Então o que muda? Bem, já estávamos fazendo algo parecido com isso, mas nossos repositories não estão separados em contratos (interfaces) e implementações e nem estamos usando use cases para acessar as implementações dos repositories e mandar os dados para a camada presentation.

Para unir todos esses módulos e fazer a arquitetura funcionar, vamos usar o framework Koin para aplicar a Dependency Inversion Principle (Princípio da Inversão da dependência), o D do SOLID. Esse princípio diz que módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração e abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

Ou seja, em várias partes do nosso código, o repository estava responsável por criar uma instância de um service, uma ViewModel estava responsável por criar uma instância de um *repository, isso muda agora.

Obs: explicação da Clean Architecture retirada do excelente artigo do Marcello Galhardo.

Usando o Koin

Koin é um framework leve de injeção de dependência totalmente escrito em Kotlin, sendo bem fácil de aprender e usar. Para usá-lo, precisamos entender as suas terminologias:

  • module: cria um módulo em Koin que pode ser usado para prover todas as dependências.
  • single: cria um singleton que pode ser usado em todo o app como uma uma instância singular.
  • factory: provê uma definição bean, a qual vai criar uma nova instância cada vez que ela é injetada.
  • get(): é usada no construtor da classe que provê a dependência necessária.

Agora vamos adicionar o Koin ao nosso projeto por meio do arquivo build.gradle:

def koinVersion = "3.2.2"

dependencies {
    ...

  // Koin
  implementation "io.insert-koin:koin-android:$koinVersion"
  implementation "io.insert-koin:koin-android-compat:$koinVersion"
  implementation "io.insert-koin:koin-androidx-workmanager:$koinVersion"
  implementation "io.insert-koin:koin-androidx-navigation:$koinVersion" 

    ...
}
Enter fullscreen mode Exit fullscreen mode

Para começarmos a usar o Koin no nosso projeto, primeiro de tudo, precisamos criar uma interface para nosso PokemonRepository, fazemos isso para obedecer aos princípios da arquitetura limpa.

package br.com.pokedex.domain.repository

import br.com.pokedex.domain.model.SinglePokemon

interface PokemonRepository {

    suspend fun getSinglePokemon(id: Int): SinglePokemon
}
Enter fullscreen mode Exit fullscreen mode

Classe PokemonRepository vai ser renomeada para PokemonRepositoryImpl e modificada:

package br.com.pokedex.data.repository

import br.com.pokedex.data.api.PokemonApi
import br.com.pokedex.data.mapper.toModel
import br.com.pokedex.domain.repository.PokemonRepository

class PokemonRepositoryImpl(private val api: PokemonApi) : PokemonRepository {

    override suspend fun getSinglePokemon(id: Int) = api.getSinglePokemon(id).toModel()
}
Enter fullscreen mode Exit fullscreen mode

Note que removemos a criação do service aqui, fizemos isso pois o Koin ficará responsável por injetar essa dependência, assim, aplicando o Princípio da Inversão da dependência.

Partiremos para a criação do use case que servirá de ponte entre o repository e a ViewModel. No momento ele será bem simples, terá apenas uma função execute() que acessará o PokemonRepository e retornará um SinglePokemon:

package br.com.pokedex.domain.interactor

import br.com.pokedex.domain.model.SinglePokemon
import br.com.pokedex.domain.repository.PokemonRepository

class GetSinglePokemonUseCase(private val repository: PokemonRepository) {

    suspend fun execute(id: Int) : SinglePokemon {
        return repository.getSinglePokemon(id)
    }
}
Enter fullscreen mode Exit fullscreen mode

Modificação em PokedexViewModel:

package br.com.pokedex.presentation

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import br.com.pokedex.domain.interactor.GetSinglePokemonUseCase
import br.com.pokedex.domain.model.SinglePokemon
import br.com.pokedex.domain.repository.PokemonRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

private const val MIN_POKEMON_ID = 1
private const val MAX_POKEMON_ID = 151

class PokedexViewModel(private val useCase: GetSinglePokemonUseCase) : ViewModel() {

    private val _pokemon = MutableLiveData<List<SinglePokemon>>()
    val pokemon: LiveData<List<SinglePokemon>>
        get() = _pokemon

    fun getPokemon() {
        viewModelScope.launch(Dispatchers.IO) {
            val data = mutableListOf<SinglePokemon>()

            for (i in MIN_POKEMON_ID..MAX_POKEMON_ID) {
                data.add(useCase.execute(i))
            }

            withContext(Dispatchers.Main) {
                _pokemon.postValue(data.toList())
            }
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Pronto, vamos começar a usar efetivamente o Koin. Inicialmente criaremos um package chamado di(abreviação para dependency injection), é nele que vamos construir nossos arquivos de injeção de dependências. Após isso, criamos o arquivo InfrastructureModule.kt, e vamos criar algumas funções que provêem instâncias de dependências importantes nele: Retrofit e PokemonApi:

package br.com.pokedex.di

import br.com.pokedex.BuildConfig
import br.com.pokedex.data.api.PokemonApi
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

private fun providePokemonApi(retrofit: Retrofit): PokemonApi {
    return retrofit.create(PokemonApi::class.java)
}

private fun provideRetrofit(): Retrofit {
    return Retrofit.Builder().run {
        addConverterFactory(GsonConverterFactory.create())
        baseUrl(BuildConfig.POKE_API)
        build()
    }
}
Enter fullscreen mode Exit fullscreen mode

Repare que em provideRetrofit() nós referenciamos uma string POKE_API, trata-se da url base da PokéAPI, fazemos isso para separá-la da lógica de negócios, nós a colocamos no arquivo build.gradle da seguinte forma:

def POKE_API = "POKE_API"
def URL_BASE_POKE_API = "\"https://pokeapi.co/api/v2/\""

android {
        ...
    buildTypes {

        debug {
            applicationIdSuffix ".dev"
            debuggable true
            buildConfigField "String", POKE_API, URL_BASE_POKE_API
        }

    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Neste momento em que já temos esses providers, podemos construir o módulo que irá prover essas dependências para a aplicação, vamos chamá-lo de InfrastructureModule, ele será uma coleção de dependências em que a instância de cada uma será retornada usando a função factory e depois injetada quando necessário for:

package br.com.pokedex.di

import br.com.pokedex.BuildConfig
import br.com.pokedex.data.api.PokemonApi
import br.com.pokedex.data.repository.PokemonRepositoryImpl
import br.com.pokedex.domain.repository.PokemonRepository
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

fun infrastructureModule() = module {
    factory { provideRetrofit() }
    factory { providePokemonApi(get()) }
    factory<PokemonRepository> { PokemonRepositoryImpl(get()) }
}

private fun providePokemonApi(retrofit: Retrofit): PokemonApi {
    return retrofit.create(PokemonApi::class.java)
}

private fun provideRetrofit(): Retrofit {
    return Retrofit.Builder().run {
        addConverterFactory(GsonConverterFactory.create())
        baseUrl(BuildConfig.POKE_API)
        build()
    }
}
Enter fullscreen mode Exit fullscreen mode

Note que programamos o módulo de tal forma que quando uma instância de PokemonRepository for requisitada, retornaremos uma instância de PokemonRepositoryImpl, fazendo com que referenciemos apenas a abstração no código e não a implementação.

Faltam apenas duas dependências: GetSinglePokemonUseCase e PokedexViewModel, procedemos com o desenvolvimento de outros dois arquivos: DomainModule.kt, para o use case, e PresentationModule.kt, para a ViewModel:

package br.com.pokedex.di

import br.com.pokedex.domain.interactor.GetSinglePokemonUseCase
import org.koin.dsl.module

fun domainModule() = module {
    factory { GetSinglePokemonUseCase(get()) }
}
Enter fullscreen mode Exit fullscreen mode
package br.com.pokedex.di

import br.com.pokedex.presentation.PokedexViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module

fun presentationModule() = module {
    viewModel { PokedexViewModel(get()) }
}
Enter fullscreen mode Exit fullscreen mode

Sendo assim, já temos os três módulos prontos para serem injetados. Para realizar essa injeção, construiremos uma classe que será a raiz do nosso projeto, a Pokedex:

package br.com.pokedex

import android.app.Application

class Pokedex : Application() {}
Enter fullscreen mode Exit fullscreen mode

Ela é subclasse de Application porque ela é a classe base do app que contém todos os outros componentes, como activities e services, ela é instanciada antes de qualquer outra classe quando o processo para a nossa aplicação é criado. E não podemos esquecer de adicioná-la ao AndroidManifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    ...

    <application
        android:name=".Pokedex"
        ...
        >

        ...

    </application>

</manifest>
Enter fullscreen mode Exit fullscreen mode

Agora vamos criar o arquivo que irá lidar com todas essas dependências, o Injector.kt, nele nós desenvolvemos uma extension function de Application chamada inject() que inicia o Koin com todas as dependências necessárias:

package br.com.pokedex.di

import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin

fun Application.inject() {
    startKoin {
        androidLogger()
        androidContext(this@inject)
        modules(getModules())
    }
}

fun getModules() = listOf(
    infrastructureModule(),
    domainModule(),
    presentationModule()
)
Enter fullscreen mode Exit fullscreen mode

Além de usarmos a função startKoin, também usamos androidLogger(), que fornece uma API simples para realizar logs relacionados ao Koin, androidContext(), o qual adiciona uma instância de Context para o contêiner do Koin, modules(), para carregar as definições dos módulos, e getModules(), que retorna todos os módulos que nós criamos.

Pronto, já podemos inserir essea função na nossa classe Application dando um override na função onCreate():

package br.com.pokedex

import android.app.Application
import br.com.pokedex.di.inject

class Pokedex : Application() {

    override fun onCreate() {
        super.onCreate()
        inject()
    }
}
Enter fullscreen mode Exit fullscreen mode

Sendo assim, nosso projeto ficou estruturado dessa forma:

Estrutura de pastas

Após rodarmos o app, vamos perceber que nada mudou visualmente, mas sabemos que nosso projeto está bem melhor estruturado e desacoplado, obedecendo aos princípios da Arquitetura Limpa!

Próximos posts

Nos próximos posts vamos melhorar a performance da nossa Pokédex usando a biblioteca Paging e a Flow API, além de deixá-las mais bonita, é claro.

Repositório no github:

GitHub logo ronaldocoding / pokedex

A simple Pokédex

Pokédex

A simple Pokédex

Post anterior:

Próximo post:

Top comments (0)