Chapter 7 - Building Data Sources - Clean Android Architecture
Chapter 7 - Building Data Sources - Clean Android Architecture
Ao final do capítulo, você terá aprendido o papel das fontes de dados, como
implementar fontes de dados remotas e locais que usam Retrofit, Room e Data
Store para gerenciar os dados de um aplicativo e como podemos separar essas
fontes de dados em módulos da biblioteca.
Requerimentos técnicos
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 1/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
Nos capítulos anteriores, definimos abstrações para fontes de dados das quais
os repositórios dependem para manipular dados. Isso ocorreu porque quería-
mos evitar que os repositórios tivessem dependências das fontes de dados e,
em vez disso, que as fontes de dados dependessem dos repositórios. Para fon-
tes de dados remotas, isso se parece com a figura a seguir:
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 2/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
interface UserRemoteDataSource {
fun getUser(id: String): Flow<User>
}
Esta é a abstração que definimos no capítulo anterior. Antes de escrevermos a
implementação desta classe, primeiro precisaremos especificar nosso serviço
de Retrofit:
interface UserService {
@GET("/users/{userId}")
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 3/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
Nesta seção, vimos como podemos construir uma fonte de dados remota com a
ajuda da biblioteca Retrofit para manipular dados da Internet. Na seção a se-
guir, veremos um exercício que mostra como podemos implementar uma
fonte de dados remota.
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 4/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
{
"id": 1,
"nome": "Leanne Graham",
"username": "Bret",
"email": "[email protected]"
}
A postagem terá a seguinte representação JSON:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident
occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae
conequuntur expedita et cum\nreprehenderit molestiae ut
ut quas totam\nnostrum rerum est autem sunt rem eveniet
architecto"
}
O módulo precisará implementar o seguinte:
mará UserService e retornará Flow , que emite uma lista de objetos User ou
UseCaseException.UserException se houver um erro na chamada para User‐
base no ID.
RemotePostDataSourceImpl que implementará RemotePostDataSource , cha-
mará PostService e retornará Flow , que emite uma listade objetos Post ou
UseCaseException.PostException se houver um erro na chamada para Post‐
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 5/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 6/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
retrofitMoshi: "com.squareup.retrofit2
:conversor-moshi:$
{versions.retrofit}",
moshi : "com.squareup.moshi:
moshi:${versions.moshi}",
moshiKotlin : "com.squareup.moshi:
moshi-kotlin:${versions.moshi}"
]
…
}
4. Dentroo arquivo build.gradle do módulo data-remote , certifique-se de que
os seguintes plugins estejam presentes:
plug-ins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
5. No mesmo arquivo, altere as configurações para as definidas no arquivo
build.gradle de nível superior :
andróide {
compileSdk defaultCompileSdkVersion
configuração padrão {
minSdk defaultMinSdkVersion
targetSdk defaultTargetSdkVersion
…
}
opções de compilação {
sourceCompileVersion javaCompileVersion
targetCompatibility javaCompileVersion
}
kotlinOptions {
jvmTarget = jvmTarget
}
}
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 7/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
Aqui, estamos nos certificando de que o novo módulo usará as mesmas confi-
gurações em relação àcompilação e a versão mínima e máxima do Android
como o restante do projeto, facilitando a alteração da configuração em todos
os módulos.
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 8/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 9/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
RemoteUserDataSource {
override fun getUsers(): Flow<List<User>> = flow {
emit(userService.getUsers())
}.map { usuários ->
users.map { userApiModel ->
convert(userApiModel)
}
}.truque {
jogue UseCaseException.UserException(it)
}
override fun getUser(id: Long): Flow<User> = flow
{
emit(userService.getUser(id))
}.map {
converta (ele)
}.truque {
jogue UseCaseException.UserException(it)
}
diversão privada convert(userApiModel:
UserApiModel) =
User(userApiModel.id, userApiModel.name,
userApiModel.username, userApiModel.email)
}
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 10/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 11/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
@Provides
fun provideMoshi(): Moshi = Moshi.Builder().add
(KotlinJsonAdapterFactory()).build()
@Provides
fun fornecerRetrofit(okHttpClient: OkHttpClient,
moshi: Moshi): Retrofit = Retrofit.Builder()
.baseUrl
("https://jsonplaceholder.typicode.com/")
.client(okHttpClient)
.addConverterFactory
(MoshiConverterFactory.create(moshi))
.construir()
@Provides
fun fornecerUserService(retrofit: Retrofit):
Serviço de usuário =
retrofit.create(UserService::class.java)
@Provides
fun providePostService(retrofit: Retrofit):
PostServiço =
retrofit.create(PostService::class.java)
}
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 12/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
RemoteUserDataSource
}
Aqui, usamos Hilt para vincular as implementações deste módulo com as abs-
trações definidas no módulo de repositório de dados .
22. Para testar a unidade do código, agora precisamos criar uma nova pasta
chamada resources na pasta de teste do módulo data-remote .
23. Dentro da pasta de recursos , crie uma pasta chamada mockito-extensions ;
dentro desta pasta, crie um arquivo chamado
org.mockito.plugins.MockMaker ; e dentro deste arquivo, adicione o se-
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 13/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 14/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
}.collect()
}
}
Aqui, estamos zombando de um erro gerado por UserService , que será con-
vertido por RemoteUserDataSourceImpl em UseCaseException.UserException .
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 15/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
Aqui, estamos fazendo para postagens o que fizemos para usuários em Remo‐
teUserDataSourceImplTest .
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 16/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
As fontes de dados locais têm uma estrutura semelhante às fontes de dados re-
motas. As abstrações são fornecidas pelas camadas acima, e as implementa-
ções são responsáveis por invocar métodos de frameworks de persistência e
converter dados em entidades, como a figura a seguir:
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 17/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
interface UserLocalDataSource {
suspender a diversão insertUser(user: User)
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 18/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
@Entity(tableName = "usuário")
classe de dados UserEntity(
@PrimaryKey @ColumnInfo(name = "id") val id: String,
@ColumnInfo(name = "first_name") val firstName:
String,
@ColumnInfo(name = "last_name") val lastName: String,
@ColumnInfo(name = "email") val email: String
)
Agora, podemos definir UserDao , que consulta um usuário por um ID e insere
um usuário:
@Dao
interface UserDao {
@Query("SELECT * FROM usuário onde id = :id")
fun getUser(id: String): Flow<UserEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
divertido insertUser(users: UserEntity)
}
finalmente, oimplementação da fonte de dados pareceisto:
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 19/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
}
}
Aqui, a fonte de dados local invoca UserDao para inserir e recuperar um usuá-
rio e converte a entidade de domínio em uma entidade Room.
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 20/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
Aqui, usamos uma chave para cada um dos campos do objeto User para arma‐
zenar os dados. O método getUser não usa o ID para procurar um usuário, o
que mostra que, para esse caso de uso específico, Room é o método mais
apropriado.
Nesta seção, vimos como podemos construir uma fonte de dados local com a
ajuda do Room eDadosArmazene bibliotecas para poder consultar e persistir
dados localmente em um dispositivo. A seguir, veremos um exercício para
mostrar como podemos implementar um armazenamento de dados local.
Modifique o Exercício 07.01 – Construindo uma fonte de dados remota para que
um novo módulo de biblioteca Androiddata-local nomeado é criado. Este mó-
dulo dependerá do domínio e do repositório de dados .
de UserEntity e PostEntity
LocalUserDataSourceImpl e LocalPostDataSourceImpl , que serão responsá-
veis por invocar os objetos UserDao e PostDao para persistir os dados e para
converter os dados em objetos User e Post
LocalInteractionDataSourceImpl , que será responsável por persistir o ob-
jeto Interaction
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 21/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 22/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
…
}
4. Dentroo arquivo build.gradle do módulo data-local , certifique-se de que
os seguintes plugins estejam presentes:
plug-ins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
5. No mesmo arquivo, altereas configurações para as definidas no arquivo
build.gradle de nível superior :
andróide {
compileSdk defaultCompileSdkVersion
configuração padrão {
minSdk defaultMinSdkVersion
targetSdk defaultTargetSdkVersion
…
}
opções de compilação {
sourceCompileVersion javaCompileVersion
targetCompatibility javaCompileVersion
}
kotlinOptions {
jvmTarget = jvmTarget
}
}
6. No mesmo arquivo, adicione as dependências às bibliotecas de rede e aos
módulos de repositório de dados e domínio :
dependências {
implementação(projeto(caminho: ":domínio"))
implementação(projeto(caminho: ":repositório de
dados"))
implementação coroutines.coroutinesAndroid
persistência de implementação.roomRuntime
persistência de implementação.roomKtx
kapt persistence.roomCompiler
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 23/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
implementação persistence.datastore
implementação di.hiltAndroid
kapt di.hiltCompiler
testImplementação test.junit
testImplementação test.coroutines
testImplementação test.mockito
}
7. No módulo local de dados , crie um novo pacote chamado db .
8. No pacote db , crieum novo pacote chamado user .
9. No pacote do usuário , crie a classe UserEntity :
@Entity(tableName = "usuário")
classe de dados UserEntity(
@PrimaryKey @ColumnInfo(name = "id") val id:
Longo,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "username") val nome de
usuário:
Corda,
@ColumnInfo(name = "email") val email: String
)
10. No mesmo pacote, crie a interface UserDao :
@Dao
interface UserDao {
@Query("SELECT * FROM usuário")
fun getUsers(): Flow<List<UserEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
divertido insertUsers(users: List<UserEntity>)
}
11. No pacote db , crie um novo pacote chamado post .
12. No pacote de postagem , crieuma nova classe chamada PostEntity :
@Entity(tableName = "post")
classe de dados PostEntity(
@PrimaryKey @ColumnInfo(name = "id") val id:
Longo,
@ColumnInfo(name = "userId") val userId: Longo,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "body") val body: String
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 24/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
)
13. No mesmo pacote, crie uma nova interface chamada PostDao :
@Dao
interface PostDao {
@Query("SELECT * FROM post")
fun getPosts(): Flow<List<PostEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
divertido insertPosts(users: List<PostEntity>)
}
14. No pacote db , crie a classe AppDatabase :
@Database(entities = [UserEntity::class,
PostEntity::class], versão = 1)
classe abstrata AppDatabase : RoomDatabase() {
diversão abstrata userDao(): UserDao
diversão abstrata postDao(): PostDao
}
15. Dentroo módulo data-local , crie um novo pacote chamado source .
16. No pacote de origem , crie uma nova classe chamada LocalUserDataSour‐
ceImpl :
class LocalUserDataSourceImpl @Inject
constructor(private val userDao: UserDao):
LocalUserDataSource {
substituir fun getUsers(): Flow<List<User>> =
userDao.getUsers().map { users ->
usuários.mapa {
User(it.id, it.name, it.username,
it.e-mail)
}
}
override suspend fun addUsers(users: List<User>) =
userDao.insertUsers(users.map {
UserEntity(it.id, it.name, it.username,
it.e-mail)
})
}
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 25/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 26/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
Aqui, usamos a biblioteca Preference Data Store para persistir o objeto Inte-
raction, mantendo chaves diferentes para cada campo da classe Interaction e,
neste caso, será apenas uma chave para o total de cliques.
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 27/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
diversão fornecerLocalInteractionDataSourceImpl
(@ApplicationContext contexto: Contexto) =
LocalInteractionDataSourceImpl(context.dataSto
re)
}
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 28/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
@ExperimentalCoroutinesApi
@Teste
fun testGetUsers() = runBlockingTest {
val localUsers = listOf(UserEntity(1, "nome",
"nome de usuário", "e-mail"))
val usuários esperados = listOf(Usuário(1,
"nome",
"nome de usuário", "e-mail"))
sempre(userDao.getUsers()).thenReturn
(flowOf(localUsers))
val resultado =
userDataSource.getUsers().first()
Assert.assertEquals(expectedUsers, result)
}
@ExperimentalCoroutinesApi
@Teste
fun testAddUsers() = runBlockingTest {
val localUsers = listOf(UserEntity(1, "nome",
"nome de usuário", "e-mail"))
val usuários = listOf(Usuário(1, "nome", "nome
de usuário",
"o email"))
userDataSource.addUsers(usuários)
verifique(userDao).insertUsers(localUsers)
}
}
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 29/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
@Teste
fun testGetPosts() = runBlockingTest {
val localPosts = listOf(PostEntity(1, 1,
"título", "corpo"))
val esperadoPosts = listOf(Post(1, 1,
"título",
"corpo"))
sempre(postDao.getPosts()).thenReturn
(flowOf(localPosts))
val resultado =
postDataSource.getPosts().first()
Assert.assertEquals(expectedPosts, result)
}
@ExperimentalCoroutinesApi
@Teste
fun testAddUsers() = runBlockingTest {
val localPosts = listOf(PostEntity(1, 1,
"título", "corpo"))
val posts = listOf(Post(1, 1, "título",
"corpo"))
postDataSource.addPosts(posts)
verifique(postDao).insertPosts(localPosts)
}
}
Aqui, realizamos o mesmo tipo de testes para postagens que fizemos em Loca‐
lUserDataSourceImplTest para usuários.
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 30/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
val cliques = 10
val interação = Interação(cliques)
val preferências = mock<Preferences>()
sempre(preferências[KEY_TOTAL_TAPS]).
entãoReturn(cliques)
sempre(dataStore.data).thenReturn
(flowOf(preferências))
val resultado = interaçãoDataSource.
getInteraction().first()
assertEquals(interação, resultado)
}
@ExperimentalCoroutinesApi
@Teste
fun testSaveInteraction() = runBlockingTest {
val cliques = 10
val interação = Interação(cliques)
val preferências = mock<MutablePreferences>()
sempre(preferences.toMutablePreferences())
.thenReturn(preferências)
sempre(dataStore.updateData(any())).
A resposta {
runBlocking {
it.getArgument<suspender
(Preferências) -
> Preferências>
(0).invoke(preferências)
}
preferências
}
interaçãoDataSource.saveInteraction(interação)
verificar(preferências)[KEY_TOTAL_TAPS] =
cliques
}
}
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 31/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
método de edição é uma função de extensão que não pode ser simulada com
as bibliotecas atuais que temos e, em vez disso, deve contar com o método que
ele invoca, que é updateData .
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 32/34
26/10/2022 21:44 Chapter 7: Building Data Sources | Clean Android Architecture
Resumo
Apoiar Sair
https://learning.oreilly.com/library/view/clean-android-architecture/9781803234588/B18320_07_ePub.xhtml 34/34