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.
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 Koinque 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" 
    ...
}
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
}
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()
}
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)
    }
}
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())
            }
        }
    }
}
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()
    }
}
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
        }
    }
    ...
}
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()
    }
}
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()) }
}
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()) }
}
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() {}
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>
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()
)
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()
    }
}
Sendo assim, nosso projeto ficou estruturado dessa forma:
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:
Post anterior:
 
    Mostrando os Pokémon com Coil em Android
Ronaldo Costa de Freitas ・ Nov 3 ・ 11 min read
Próximo post:
 
 
              


 
    
Top comments (0)