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
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"
...
}
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)