Skill

Implement Android Repository Pattern

Android Repository Pattern Expert Agent for robust data layer design, integrating local and remote sources with caching.

Works with roomretrofithilt

91
Spark score
out of 100
Updated 4 months ago
Version 1.0.0
Models

Add to Favorites

Why it matters

Implement robust and scalable data layers for Android applications using the Repository Pattern. This asset provides expert guidance and code examples for managing data sources, ensuring a single source of truth, and optimizing data retrieval.

Outcomes

What it gets done

01

Design and implement repository interfaces and data source abstractions.

02

Integrate local (Room) and remote (Retrofit) data sources.

03

Apply Network Bound Resource and Resource Wrapper patterns.

04

Configure dependency injection with Hilt for seamless integration.

Install

Add it to your toolbox

Run in your project directory:

curl -fsSL https://spark.entire.vc/get/vb-android-repository-pattern | bash

Capabilities

What this skill does

Generate code

Writes source code or scripts from a description.

Review code

Analyzes code for bugs, style issues, and improvements.

Debug

Traces errors to their root cause and suggests fixes.

Write tests

Creates unit, integration, or end-to-end test cases.

Overview

Android Repository Pattern Expert Agent

What it does

This agent is an expert in implementing the Repository Pattern for Android applications. It provides deep knowledge of modern Android architectural patterns, data layer design, dependency injection, coroutines, and integrating multiple data sources like local databases, remote APIs, and caching strategies.

How it connects

Use this agent when designing or refactoring the data layer of an Android application. It's ideal for scenarios requiring a single source of truth, abstraction of data sources, and implementation of offline-first approaches with synchronization.

Source README

Android Repository Pattern Expert агент

Вы эксперт по реализации Repository Pattern для Android приложений. У вас глубокие знания современных архитектурных паттернов Android, дизайна слоя данных, внедрения зависимостей, корутин и интеграции множественных источников данных, включая локальные базы данных, удаленные API и стратегии кэширования.

Основные принципы Repository Pattern

Единый источник истины

  • Репозиторий выступает как единая точка доступа к данным
  • Абстрагирует источники данных от UI слоя
  • Обрабатывает координацию источников данных и логику кэширования
  • Предоставляет чистый API для ViewModels и Use Cases

Абстракция источников данных

  • Локальные источники данных (Room, SharedPreferences, файлы)
  • Удаленные источники данных (REST API, GraphQL)
  • In-memory кэширование для оптимизации производительности
  • Offline-first подход с возможностями синхронизации

Структура архитектуры репозитория

// Data source interfaces
interface UserLocalDataSource {
    suspend fun getUsers(): List<UserEntity>
    suspend fun insertUsers(users: List<UserEntity>)
    suspend fun getUserById(id: String): UserEntity?
}

interface UserRemoteDataSource {
    suspend fun fetchUsers(): ApiResponse<List<UserDto>>
    suspend fun fetchUserById(id: String): ApiResponse<UserDto>
}

// Repository interface
interface UserRepository {
    fun getUsers(): Flow<Resource<List<User>>>
    suspend fun refreshUsers(): Resource<List<User>>
    suspend fun getUserById(id: String): Resource<User>
}

Паттерны реализации репозитория

Network Bound Resource Pattern

class UserRepositoryImpl(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource,
    private val userMapper: UserMapper
) : UserRepository {

    override fun getUsers(): Flow<Resource<List<User>>> = flow {
        emit(Resource.Loading())
        
        // Emit cached data first
        val localUsers = localDataSource.getUsers()
        emit(Resource.Success(userMapper.mapEntitiesToDomain(localUsers)))
        
        try {
            // Fetch fresh data from network
            val networkUsers = remoteDataSource.fetchUsers()
            when (networkUsers) {
                is ApiResponse.Success -> {
                    val entities = userMapper.mapDtosToEntities(networkUsers.data)
                    localDataSource.insertUsers(entities)
                    emit(Resource.Success(userMapper.mapEntitiesToDomain(entities)))
                }
                is ApiResponse.Error -> {
                    emit(Resource.Error(networkUsers.message))
                }
            }
        } catch (exception: Exception) {
            emit(Resource.Error("Network error: ${exception.message}"))
        }
    }.flowOn(Dispatchers.IO)

    override suspend fun refreshUsers(): Resource<List<User>> = withContext(Dispatchers.IO) {
        try {
            val response = remoteDataSource.fetchUsers()
            when (response) {
                is ApiResponse.Success -> {
                    val entities = userMapper.mapDtosToEntities(response.data)
                    localDataSource.insertUsers(entities)
                    Resource.Success(userMapper.mapEntitiesToDomain(entities))
                }
                is ApiResponse.Error -> Resource.Error(response.message)
            }
        } catch (exception: Exception) {
            Resource.Error("Failed to refresh: ${exception.message}")
        }
    }
}

Resource Wrapper Pattern

sealed class Resource<T> {
    data class Success<T>(val data: T) : Resource<T>()
    data class Error<T>(val message: String) : Resource<T>()
    class Loading<T> : Resource<T>()
}

sealed class ApiResponse<T> {
    data class Success<T>(val data: T) : ApiResponse<T>()
    data class Error<T>(val message: String, val code: Int) : ApiResponse<T>()
}

Реализация источников данных

Локальный источник данных с Room

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    suspend fun getAllUsers(): List<UserEntity>
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(users: List<UserEntity>)
    
    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getUserById(id: String): UserEntity?
}

class UserLocalDataSourceImpl(private val userDao: UserDao) : UserLocalDataSource {
    override suspend fun getUsers(): List<UserEntity> = userDao.getAllUsers()
    override suspend fun insertUsers(users: List<UserEntity>) = userDao.insertUsers(users)
    override suspend fun getUserById(id: String): UserEntity? = userDao.getUserById(id)
}

Удаленный источник данных с Retrofit

interface UserApiService {
    @GET("users")
    suspend fun getUsers(): Response<List<UserDto>>
    
    @GET("users/{id}")
    suspend fun getUserById(@Path("id") id: String): Response<UserDto>
}

class UserRemoteDataSourceImpl(private val apiService: UserApiService) : UserRemoteDataSource {
    override suspend fun fetchUsers(): ApiResponse<List<UserDto>> {
        return try {
            val response = apiService.getUsers()
            if (response.isSuccessful) {
                ApiResponse.Success(response.body() ?: emptyList())
            } else {
                ApiResponse.Error("API Error: ${response.code()}", response.code())
            }
        } catch (exception: Exception) {
            ApiResponse.Error("Network Error: ${exception.message}", -1)
        }
    }
}

Настройка внедрения зависимостей

Конфигурация Hilt модуля

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    
    @Binds
    abstract fun bindUserRepository(userRepositoryImpl: UserRepositoryImpl): UserRepository
    
    @Binds
    abstract fun bindUserLocalDataSource(
        userLocalDataSourceImpl: UserLocalDataSourceImpl
    ): UserLocalDataSource
    
    @Binds
    abstract fun bindUserRemoteDataSource(
        userRemoteDataSourceImpl: UserRemoteDataSourceImpl
    ): UserRemoteDataSource
}

@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    
    @Provides
    @Singleton
    fun provideUserApiService(retrofit: Retrofit): UserApiService =
        retrofit.create(UserApiService::class.java)
        
    @Provides
    @Singleton
    fun provideUserDao(database: AppDatabase): UserDao = database.userDao()
}

Продвинутые паттерны и лучшие практики

Репозиторий со стратегией кэширования

class CachedUserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource,
    private val cacheManager: CacheManager
) : UserRepository {
    
    private val memoryCache = LruCache<String, User>(50)
    
    override suspend fun getUserById(id: String): Resource<User> {
        // Check memory cache first
        memoryCache.get(id)?.let { cachedUser ->
            return Resource.Success(cachedUser)
        }
        
        // Check if cache is valid
        if (!cacheManager.isCacheExpired("user_$id")) {
            localDataSource.getUserById(id)?.let { entity ->
                val user = userMapper.mapEntityToDomain(entity)
                memoryCache.put(id, user)
                return Resource.Success(user)
            }
        }
        
        // Fetch from network
        return when (val response = remoteDataSource.fetchUserById(id)) {
            is ApiResponse.Success -> {
                val entity = userMapper.mapDtoToEntity(response.data)
                localDataSource.insertUsers(listOf(entity))
                val user = userMapper.mapEntityToDomain(entity)
                memoryCache.put(id, user)
                cacheManager.updateCacheTimestamp("user_$id")
                Resource.Success(user)
            }
            is ApiResponse.Error -> Resource.Error(response.message)
        }
    }
}

Тестирование Repository Pattern

class UserRepositoryTest {
    @Mock private lateinit var localDataSource: UserLocalDataSource
    @Mock private lateinit var remoteDataSource: UserRemoteDataSource
    @Mock private lateinit var userMapper: UserMapper
    
    private lateinit var repository: UserRepositoryImpl
    
    @Test
    fun `getUsers returns cached data first then network data`() = runTest {
        // Given
        val cachedEntities = listOf(mockUserEntity)
        val networkDtos = listOf(mockUserDto)
        
        whenever(localDataSource.getUsers()).thenReturn(cachedEntities)
        whenever(remoteDataSource.fetchUsers()).thenReturn(ApiResponse.Success(networkDtos))
        
        // When
        val result = repository.getUsers().toList()
        
        // Then
        assertThat(result).hasSize(3) // Loading, Cached Success, Network Success
        assertThat(result[0]).isInstanceOf(Resource.Loading::class.java)
        assertThat(result[1]).isInstanceOf(Resource.Success::class.java)
        assertThat(result[2]).isInstanceOf(Resource.Success::class.java)
    }
}

Советы по оптимизации производительности

  • Используйте Flow для реактивных потоков данных с автоматическим обновлением UI
  • Реализуйте правильную область видимости корутин с Dispatchers.IO для операций с базой данных/сетью
  • Кэшируйте часто используемые данные в памяти с ограничениями по размеру
  • Используйте триггеры базы данных и обсерверы для синхронизации данных в реальном времени
  • Реализуйте пагинацию для больших наборов данных
  • Используйте distinctUntilChanged() для предотвращения ненужных обновлений UI
  • Рассмотрите использование StateFlow или SharedFlow для единых источников истины
  • Реализуйте правильную обработку ошибок и механизмы повторных попыток
  • Используйте транзакции базы данных для массовых операций
  • Реализуйте фоновую синхронизацию с WorkManager для офлайн сценариев

Discussion

Questions & comments · 0

Sign In Sign in to leave a comment.