본문 바로가기

안드로이드/Compose Paging3

[안드로이드 Jetpack Compose Paging3 시리즈] Local Data Source 생성하기 #8


1. 로컬 데이터베이스에 접근하기 위한 Local Data Source

안녕하세요. 지난 포스팅에서는 안드로이드 Jetpack Compose Paging3 라이브러리를 사용할 때 로컬 데이터베이스에 캐시 데이터를 저장하기 위해서 Room 라이브러리를 이용하여 데이터베이스를 세팅해 주었습니다. 이번 포스팅에서는 데이터베이스와 상호작용을 하기 위한 Local Data Source를 세팅하겠습니다.

Local Data Source 관련 파일
안드로이드 Jetpack Compose의 Paging3 라이브러리에서 RemoteMediator를 사용하기 위한 Local Data Source 관련 파일

2. Local Data Source 세팅하기

RemoteMediator는 안드로이드 Jetpack Compose의 Paging3 라이브러리의 핵심 컴포넌트 중 하나로, 네트워크와 로컬 데이터베이스 간의 데이터 동기화를 담당합니다. 그래서 데이터베이스와 상호작용을 할 수 있는 Local Data Source를 먼저 구현해주어야 합니다. 네트워크에서 데이터를 가져온 후 로컬 데이터베이스에 저장하고, 이 데이터를 PagingSource를 통해 UI에 제공하는 역할을 하기 때문입니다.

2-1. RemoteMediator에서 사용할 LocalDataSource 생성

이전 포스팅에서 생성한 데이터베이스에 접근할 수 있는 Local Data Source를 생성하겠습니다. Data Source는 클린 아키텍처의 Data Layer에서 네트워크나 로컬 데이터베이스와의 연결을 담당합니다.

// MovieDataSource.kt
interface MovieDataSource {
    interface Remote {
        suspend fun getMovies(page: Int, limit: Int): ApiResult<List<MovieEntity>>
        suspend fun getMovie(movieId: Int): ApiResult<MovieEntity>
    }

    interface Local {
        fun pagingSource(): PagingSource<Int, MovieDbData>
        suspend fun getMovies(): ApiResult<List<MovieEntity>>
        suspend fun saveMovies(movieEntities: List<MovieEntity>)
        suspend fun getLastRemoteKey(): MovieRemoteKeyDbData?
        suspend fun saveRemoteKey(key: MovieRemoteKeyDbData)
        suspend fun clearMovies()
        suspend fun clearRemoteKeys()
    }
}
// MovieDataMapper.kt
fun MovieEntity.toDbData() = MovieDbData(
    id = id,
    image = image,
    description = description,
    title = title,
    category = category,
    backgroundUrl = backgroundUrl
)

// DiskExecutor.kt
class DiskExecutor : Executor {
    private val executor: Executor = Executors.newSingleThreadExecutor()
    override fun execute(runnable: Runnable) {
        executor.execute(runnable)
    }
}

// MovieLocalDataSource.kt
class MovieLocalDataSource(
    private val executor: DiskExecutor,
    private val movieDao: MovieDao,
    private val remoteKeyDao: MovieRemoteKeyDao,
) : MovieDataSource.Local {

    override fun pagingSource(): PagingSource<Int, MovieDbData> = movieDao.pagingSource()

    override suspend fun getMovies(): ApiResult<List<MovieEntity>> = withContext(executor.asCoroutineDispatcher()) {
        val movies = movieDao.getMovies()
        return@withContext if (movies.isNotEmpty()) {
            ApiResult.Success(movies.map { it.toDomain() })
        } else {
            ApiResult.Error(Throwable("Data Not Available"))
        }
    }
    override suspend fun saveMovies(movieEntities: List<MovieEntity>) = withContext(executor.asCoroutineDispatcher()) {
        movieDao.saveMovies(movieEntities.map { it.toDbData() })
    }

    override suspend fun getLastRemoteKey(): MovieRemoteKeyDbData? = withContext(executor.asCoroutineDispatcher()) {
        remoteKeyDao.getLastRemoteKey()
    }

    override suspend fun saveRemoteKey(key: MovieRemoteKeyDbData) = withContext(executor.asCoroutineDispatcher()) {
        remoteKeyDao.saveRemoteKey(key)
    }

    override suspend fun clearMovies() = withContext(executor.asCoroutineDispatcher()) {
        movieDao.clearMoviesExceptFavorites()
    }

    override suspend fun clearRemoteKeys() = withContext(executor.asCoroutineDispatcher()) {
        remoteKeyDao.clearRemoteKeys()
    }
}

2-2. MovieRepository 생성자에 Local Data Source 추가하기

MovieRepository에서 Local Data Source에 접근할 수 있도록, 위에서 정의한 Local Data Source를 생성자에 추가합니다. 현재 movies 함수는 이전에 구현했던 네트워크 통신 기반의 페이징 처리를 구현한 부분인데 다음 포스팅에서 RemoteMediator를 적용한 함수로 변경할 예정입니다.

class MovieRepositoryImpl(
    private val remote: MovieDataSource.Remote,
    private val local: MovieDataSource.Local,
) : MovieRepository {
    override fun movies(pageSize: Int): Flow<PagingData<MovieEntity>> {
        return Pager(
            config = PagingConfig(
                pageSize = pageSize,
                enablePlaceholders = false,
            ),
            remoteMediator = // RemoteMediator 전달해야함
            pagingSourceFactory = { MoviesPagingSource(remote) }
        ).flow
    }

    override suspend fun getMovieDetail(movieId: Int): ApiResult<MovieEntity> =
        remote.getMovie(movieId)
}

 

2-3. Local Data Source 관련 의존성 주입하기

생성자로 전달받는 Local Data Source, MovieDao 등을 사용하기 위해서 의존성 주입을 해주어야 합니다. 본 시리즈는 클린 아키텍처 시리즈의 프로젝트에서 이어서 하는 중이기 때문에 Hilt 모듈을 추가하겠습니다.

// DatabaseModule.kt (UI Layer)
@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
    @Provides
    @Singleton
    fun provideMovieDatabase(@ApplicationContext context: Context): MovieDatabase {
        return Room.databaseBuilder(context, MovieDatabase::class.java, "movie.db").build()
    }

    @Provides
    fun provideMovieDao(movieDatabase: MovieDatabase): MovieDao {
        return movieDatabase.movieDao()
    }

    @Provides
    fun provideMovieRemoteKeyDao(movieDatabase: MovieDatabase): MovieRemoteKeyDao {
        return movieDatabase.movieRemoteKeyDao()
    }
}

// DataModule.kt (UI Layer)
@Module
@InstallIn(SingletonComponent::class)
class DataModule {
    @Provides
    fun provideDiskExecutor(): DiskExecutor {
        return DiskExecutor()
    }

    @Provides
    @Singleton
    fun provideMovieRepository(
        movieRemote: MovieDataSource.Remote,
        movieLocal: MovieDataSource.Local,
    ): MovieRepository {
        return MovieRepositoryImpl(
            remote = movieRemote,
            local = movieLocal
        )
    }


    @Provides
    @Singleton
    fun provideMovieLocalDataSource(
        executor: DiskExecutor,
        movieDao: MovieDao,
        movieRemoteKeyDao: MovieRemoteKeyDao,
    ): MovieDataSource.Local {
        return MovieLocalDataSource(executor, movieDao, movieRemoteKeyDao)
    }
    
    ...
}

3. Local Data Source 정리

이번 포스팅에서는 안드로이드 Jetpack Compose Paging3에서 RemoteMediator를 구현하기 위한 사전 작업을 해주었습니다. 지난 포스팅에서 세팅한 Room 데이터베이스를 사용하기 위해서 Local Data Source를 만들고 의존성 주입까지 해주었습니다.

이제 Paging3 라이브러리의 RemoteMediator를 구현하는 과정만 남았습니다.
다음 포스팅에 이어서 진행하겠습니다.

반응형