Initial commit
This commit is contained in:
		
							
								
								
									
										1
									
								
								data/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								data/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| build/ | ||||
							
								
								
									
										34
									
								
								data/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								data/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed | ||||
| plugins { | ||||
|     alias(libs.plugins.convention.android.library) | ||||
|     alias(libs.plugins.kotlin.serialization) | ||||
| } | ||||
|  | ||||
| android { | ||||
|     namespace = "dev.adriankuta.pixabay.data" | ||||
|     buildFeatures { | ||||
|         buildConfig = true | ||||
|     } | ||||
|  | ||||
|     defaultConfig { | ||||
|         buildConfigField("String", "PIXABAY_API_KEY", "\"<REPLACE_WITH_PIXABAY_API_KEY>\"") | ||||
|     } | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation(libs.retrofit) | ||||
|  | ||||
|     implementation(platform(libs.okhttp.bom)) | ||||
|     implementation(libs.okhttp) | ||||
|     implementation(libs.logging.interceptor) | ||||
|  | ||||
|     implementation(libs.androidx.paging.compose) | ||||
|  | ||||
|     implementation(libs.kotlinx.serialization.json) | ||||
|     implementation(libs.retrofit2.kotlinx.serialization.converter) | ||||
|  | ||||
|     implementation(libs.androidx.room.runtime) | ||||
|     ksp(libs.androidx.room.compiler) | ||||
|     implementation(libs.androidx.room.ktx) | ||||
|     implementation(libs.androidx.room.paging) | ||||
| } | ||||
| @@ -0,0 +1,52 @@ | ||||
| package dev.adriankuta.pixabay.data.di | ||||
|  | ||||
| import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory | ||||
| import dagger.Module | ||||
| import dagger.Provides | ||||
| import dagger.hilt.InstallIn | ||||
| import dagger.hilt.components.SingletonComponent | ||||
| import dev.adriankuta.pixabay.data.BuildConfig | ||||
| import dev.adriankuta.pixabay.data.network.PixabayService | ||||
| import kotlinx.serialization.json.Json | ||||
| import okhttp3.MediaType.Companion.toMediaType | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.logging.HttpLoggingInterceptor | ||||
| import retrofit2.Retrofit | ||||
| import javax.inject.Singleton | ||||
|  | ||||
| @InstallIn(SingletonComponent::class) | ||||
| @Module | ||||
| internal class NetworkModule { | ||||
|     @Singleton | ||||
|     @Provides | ||||
|     fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor { | ||||
|         val logging = HttpLoggingInterceptor() | ||||
|         logging.level = | ||||
|             if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE | ||||
|         return logging | ||||
|     } | ||||
|  | ||||
|     @Singleton | ||||
|     @Provides | ||||
|     fun provideOkHttpClient( | ||||
|         httpLoggingInterceptor: HttpLoggingInterceptor | ||||
|     ): OkHttpClient { | ||||
|         return OkHttpClient.Builder() | ||||
|             .addInterceptor(httpLoggingInterceptor) | ||||
|             .build() | ||||
|     } | ||||
|  | ||||
|     @Singleton | ||||
|     @Provides | ||||
|     fun providePixabayApi( | ||||
|         okHttpClient: OkHttpClient | ||||
|     ): PixabayService { | ||||
|         val networkJson = Json { ignoreUnknownKeys = true } | ||||
|         return Retrofit.Builder() | ||||
|             .client(okHttpClient) | ||||
|             .baseUrl("https://pixabay.com/") | ||||
|             .addConverterFactory(networkJson.asConverterFactory("application/json".toMediaType())) | ||||
|             .build() | ||||
|             .create(PixabayService::class.java) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,18 @@ | ||||
| package dev.adriankuta.pixabay.data.di | ||||
|  | ||||
| import dagger.Binds | ||||
| import dagger.Module | ||||
| import dagger.hilt.InstallIn | ||||
| import dagger.hilt.components.SingletonComponent | ||||
| import dev.adriankuta.pixabay.data.repository.ImageRepository | ||||
| import dev.adriankuta.pixabay.data.repository.PixabayImageRepository | ||||
|  | ||||
| @InstallIn(SingletonComponent::class) | ||||
| @Module | ||||
| internal abstract class NetworkRepositoresModule { | ||||
|  | ||||
|     @Binds | ||||
|     abstract fun bindPhotoRepository(pixabayImageRepository: PixabayImageRepository): ImageRepository | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| package dev.adriankuta.pixabay.data.di | ||||
|  | ||||
| import android.content.Context | ||||
| import androidx.room.Room | ||||
| import dagger.Module | ||||
| import dagger.Provides | ||||
| import dagger.hilt.InstallIn | ||||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||||
| import dagger.hilt.components.SingletonComponent | ||||
| import dev.adriankuta.pixabay.data.room.AppDatabase | ||||
| import dev.adriankuta.pixabay.data.room.dao.PixabayImagesDao | ||||
|  | ||||
| @InstallIn(SingletonComponent::class) | ||||
| @Module | ||||
| internal class PersistanceModule { | ||||
|     @Provides | ||||
|     fun provideRoomDb(@ApplicationContext context: Context): AppDatabase { | ||||
|         return Room.databaseBuilder( | ||||
|             context, | ||||
|             AppDatabase::class.java, | ||||
|             "pixabay.db" | ||||
|         ).build() | ||||
|     } | ||||
|  | ||||
|     @Provides | ||||
|     fun providePixabayImagesDao(appDatabase: AppDatabase) = appDatabase.imagesDao() | ||||
|  | ||||
|     @Provides | ||||
|     fun provideRemoteKeysDao(appDatabase: AppDatabase) = appDatabase.remoteKeysDao() | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,28 @@ | ||||
| package dev.adriankuta.pixabay.data.dto.response | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| internal data class PixabayImageResponse( | ||||
|     val id: Int, | ||||
|     val pageURL: String, | ||||
|     val type: String, | ||||
|     val tags: String, | ||||
|     val previewURL: String, | ||||
|     val previewWidth: Int, | ||||
|     val previewHeight: Int, | ||||
|     val webformatURL: String, | ||||
|     val webformatWidth: Int, | ||||
|     val webformatHeight: Int, | ||||
|     val largeImageURL: String, | ||||
|     val imageWidth: Int, | ||||
|     val imageHeight: Int, | ||||
|     val imageSize: Int, | ||||
|     val views: Int, | ||||
|     val downloads: Int, | ||||
|     val likes: Int, | ||||
|     val comments: Int, | ||||
|     val user_id: Int, | ||||
|     val user: String, | ||||
|     val userImageURL: String | ||||
| ) | ||||
| @@ -0,0 +1,8 @@ | ||||
| package dev.adriankuta.pixabay.data.dto.response | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| internal data class SearchPixabayImagesResponse( | ||||
|     val hits: List<PixabayImageResponse> | ||||
| ) | ||||
| @@ -0,0 +1,78 @@ | ||||
| package dev.adriankuta.pixabay.data.model | ||||
|  | ||||
| import dev.adriankuta.pixabay.data.dto.response.PixabayImageResponse | ||||
| import dev.adriankuta.pixabay.data.room.entity.PixabayImageEntity | ||||
|  | ||||
| data class PixabayImage( | ||||
|     val id: Int, | ||||
|     val pageURL: String, | ||||
|     val type: String, | ||||
|     val tags: String, | ||||
|     val previewURL: String, | ||||
|     val previewWidth: Int, | ||||
|     val previewHeight: Int, | ||||
|     val webformatURL: String, | ||||
|     val webformatWidth: Int, | ||||
|     val webformatHeight: Int, | ||||
|     val largeImageURL: String, | ||||
|     val imageWidth: Int, | ||||
|     val imageHeight: Int, | ||||
|     val imageSize: Int, | ||||
|     val views: Int, | ||||
|     val downloads: Int, | ||||
|     val likes: Int, | ||||
|     val comments: Int, | ||||
|     val user_id: Int, | ||||
|     val user: String, | ||||
|     val userImageURL: String | ||||
| ) { | ||||
|     internal constructor(entity: PixabayImageEntity) : | ||||
|             this( | ||||
|                 id = entity.id, | ||||
|                 pageURL = entity.pageURL, | ||||
|                 type = entity.type, | ||||
|                 tags = entity.tags, | ||||
|                 previewURL = entity.previewURL, | ||||
|                 previewWidth = entity.previewWidth, | ||||
|                 previewHeight = entity.previewHeight, | ||||
|                 webformatURL = entity.webformatURL, | ||||
|                 webformatWidth = entity.webformatWidth, | ||||
|                 webformatHeight = entity.webformatHeight, | ||||
|                 largeImageURL = entity.largeImageURL, | ||||
|                 imageWidth = entity.imageWidth, | ||||
|                 imageHeight = entity.imageHeight, | ||||
|                 imageSize = entity.imageSize, | ||||
|                 views = entity.views, | ||||
|                 downloads = entity.downloads, | ||||
|                 likes = entity.likes, | ||||
|                 comments = entity.comments, | ||||
|                 user_id = entity.userId, | ||||
|                 user = entity.user, | ||||
|                 userImageURL = entity.userImageURL | ||||
|             ) | ||||
|  | ||||
|     internal constructor(entity: PixabayImageResponse) : | ||||
|             this( | ||||
|                 id = entity.id, | ||||
|                 pageURL = entity.pageURL, | ||||
|                 type = entity.type, | ||||
|                 tags = entity.tags, | ||||
|                 previewURL = entity.previewURL, | ||||
|                 previewWidth = entity.previewWidth, | ||||
|                 previewHeight = entity.previewHeight, | ||||
|                 webformatURL = entity.webformatURL, | ||||
|                 webformatWidth = entity.webformatWidth, | ||||
|                 webformatHeight = entity.webformatHeight, | ||||
|                 largeImageURL = entity.largeImageURL, | ||||
|                 imageWidth = entity.imageWidth, | ||||
|                 imageHeight = entity.imageHeight, | ||||
|                 imageSize = entity.imageSize, | ||||
|                 views = entity.views, | ||||
|                 downloads = entity.downloads, | ||||
|                 likes = entity.likes, | ||||
|                 comments = entity.comments, | ||||
|                 user_id = entity.user_id, | ||||
|                 user = entity.user, | ||||
|                 userImageURL = entity.userImageURL | ||||
|             ) | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| package dev.adriankuta.pixabay.data.network | ||||
|  | ||||
| import dev.adriankuta.pixabay.data.BuildConfig | ||||
| import dev.adriankuta.pixabay.data.dto.response.SearchPixabayImagesResponse | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Query | ||||
|  | ||||
| internal interface PixabayService { | ||||
|  | ||||
|     @GET("api") | ||||
|     suspend fun searchImages( | ||||
|         @Query("q") query: String, | ||||
|         @Query("page") page: Int, | ||||
|         @Query("per_page") pageSize: Int, | ||||
|         @Query("key") key: String = BuildConfig.PIXABAY_API_KEY | ||||
|     ): SearchPixabayImagesResponse | ||||
|  | ||||
|     @GET("api") | ||||
|     suspend fun searchImageById( | ||||
|         @Query("id") query: String, | ||||
|         @Query("key") key: String = BuildConfig.PIXABAY_API_KEY | ||||
|     ): SearchPixabayImagesResponse | ||||
| } | ||||
| @@ -0,0 +1,52 @@ | ||||
| package dev.adriankuta.pixabay.data.paging | ||||
|  | ||||
| import androidx.paging.PagingSource | ||||
| import androidx.paging.PagingState | ||||
| import dev.adriankuta.pixabay.data.model.PixabayImage | ||||
| import dev.adriankuta.pixabay.data.network.PixabayService | ||||
| import dev.adriankuta.pixabay.data.repository.PixabayImageRepository.Companion.NETWORK_PAGE_SIZE | ||||
| import retrofit2.HttpException | ||||
| import java.io.IOException | ||||
|  | ||||
| private const val STARTING_PAGE_INDEX = 1 | ||||
|  | ||||
| internal class PixabayPagingSource( | ||||
|     private val pixabayService: PixabayService, | ||||
|     private val query: String | ||||
| ) : PagingSource<Int, PixabayImage>() { | ||||
|  | ||||
|     override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PixabayImage> { | ||||
|         val position = params.key ?: STARTING_PAGE_INDEX | ||||
|         return try { | ||||
|             val response = pixabayService.searchImages(query, position, params.loadSize) | ||||
|             val results = response.hits.map { PixabayImage(it) } | ||||
|             val nextKey = if (results.isEmpty()) { | ||||
|                 null | ||||
|             } else { | ||||
|                 // initial load size = 3 * NETWORK_PAGE_SIZE | ||||
|                 // ensure we're not requesting duplicating items, at the 2nd request | ||||
|                 position + (params.loadSize / NETWORK_PAGE_SIZE) | ||||
|             } | ||||
|             LoadResult.Page( | ||||
|                 data = results, | ||||
|                 prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1, | ||||
|                 nextKey = nextKey | ||||
|             ) | ||||
|         } catch (exception: IOException) { | ||||
|             return LoadResult.Error(exception) | ||||
|         } catch (exception: HttpException) { | ||||
|             return LoadResult.Error(exception) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // The refresh key is used for subsequent refresh calls to PagingSource.load after the initial load | ||||
|     override fun getRefreshKey(state: PagingState<Int, PixabayImage>): Int? { | ||||
|         // We need to get the previous key (or next key if previous is null) of the page | ||||
|         // that was closest to the most recently accessed index. | ||||
|         // Anchor position is the most recently accessed index | ||||
|         return state.anchorPosition?.let { anchorPosition -> | ||||
|             state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) | ||||
|                 ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,111 @@ | ||||
| @file:OptIn(ExperimentalPagingApi::class) | ||||
|  | ||||
| package dev.adriankuta.pixabay.data.paging | ||||
|  | ||||
| import androidx.paging.ExperimentalPagingApi | ||||
| import androidx.paging.LoadType | ||||
| import androidx.paging.PagingState | ||||
| import androidx.paging.RemoteMediator | ||||
| import androidx.room.withTransaction | ||||
| import dev.adriankuta.pixabay.data.model.PixabayImage | ||||
| import dev.adriankuta.pixabay.data.network.PixabayService | ||||
| import dev.adriankuta.pixabay.data.room.AppDatabase | ||||
| import dev.adriankuta.pixabay.data.room.entity.PixabayImageEntity | ||||
| import dev.adriankuta.pixabay.data.room.entity.RemoteKeys | ||||
| import retrofit2.HttpException | ||||
| import java.io.IOException | ||||
|  | ||||
| private const val STARTING_PAGE_INDEX = 1 | ||||
|  | ||||
| internal class PixabayRemoteMediator( | ||||
|     private val query: String, | ||||
|     private val database: AppDatabase, | ||||
|     private val pixabayService: PixabayService | ||||
| ) : RemoteMediator<Int, PixabayImageEntity>() { | ||||
|  | ||||
|     private val imagesDao = database.imagesDao() | ||||
|     private val remoteKeysDao = database.remoteKeysDao() | ||||
|  | ||||
|  | ||||
|     override suspend fun load( | ||||
|         loadType: LoadType, | ||||
|         state: PagingState<Int, PixabayImageEntity> | ||||
|     ): MediatorResult { | ||||
|         val page = when (loadType) { | ||||
|             LoadType.REFRESH -> { | ||||
|                 val remoteKeys = getRemoteKeyClosestToCurrentPosition(state) | ||||
|                 remoteKeys?.nextKey?.minus(1) ?: STARTING_PAGE_INDEX | ||||
|             } | ||||
|  | ||||
|             LoadType.PREPEND -> { | ||||
|                 val remoteKeys = getRemoteKeyForFirstItem(state) | ||||
|                 // If remoteKeys is null, that means the refresh result is not in the database yet. | ||||
|                 val prevKey = remoteKeys?.prevKey | ||||
|                     ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) | ||||
|                 prevKey | ||||
|             } | ||||
|  | ||||
|             LoadType.APPEND -> { | ||||
|                 val remoteKeys = getRemoteKeyForLastItem(state) | ||||
|                 // If remoteKeys is null, that means the refresh result is not in the database yet. | ||||
|                 // We can return Success with endOfPaginationReached = false because Paging | ||||
|                 // will call this method again if RemoteKeys becomes non-null. | ||||
|                 // If remoteKeys is NOT NULL but its nextKey is null, that means we've reached | ||||
|                 // the end of pagination for append. | ||||
|                 val nextKey = remoteKeys?.nextKey | ||||
|                     ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) | ||||
|                 nextKey | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             val response = pixabayService.searchImages(query, page, state.config.pageSize) | ||||
|             val imageEntities = response.hits.map { PixabayImageEntity(it, query) } | ||||
|             val endOfPaginationReached = response.hits.isEmpty() | ||||
|             database.withTransaction { | ||||
|                 if (loadType == LoadType.REFRESH) { | ||||
|                     imagesDao.clearAll() | ||||
|                     remoteKeysDao.clearRemoteKeys() | ||||
|                 } | ||||
|                 val prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1 | ||||
|                 val nextKey = if (endOfPaginationReached) null else page + 1 | ||||
|                 val keys = imageEntities.map { | ||||
|                     RemoteKeys(id = it.id, prevKey = prevKey, nextKey = nextKey) | ||||
|                 } | ||||
|                 remoteKeysDao.insertAll(keys) | ||||
|                 imagesDao.insertAll(imageEntities) | ||||
|             } | ||||
|             return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) | ||||
|         } catch (exception: IOException) { | ||||
|             return MediatorResult.Error(exception) | ||||
|         } catch (exception: HttpException) { | ||||
|             return MediatorResult.Error(exception) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, PixabayImageEntity>): RemoteKeys? { | ||||
|         return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull() | ||||
|             ?.let { pixabayImage -> | ||||
|                 remoteKeysDao.remoteKeysImageId(pixabayImage.id) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, PixabayImageEntity>): RemoteKeys? { | ||||
|         return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull() | ||||
|             ?.let { pixabayImage -> | ||||
|                 remoteKeysDao.remoteKeysImageId(pixabayImage.id) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private suspend fun getRemoteKeyClosestToCurrentPosition( | ||||
|         state: PagingState<Int, PixabayImageEntity> | ||||
|     ): RemoteKeys? { | ||||
|         // The paging library is trying to load data after the anchor position | ||||
|         // Get the item closest to the anchor position | ||||
|         return state.anchorPosition?.let { position -> | ||||
|             state.closestItemToPosition(position)?.id?.let { imageId -> | ||||
|                 remoteKeysDao.remoteKeysImageId(imageId) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| package dev.adriankuta.pixabay.data.repository | ||||
|  | ||||
| import androidx.paging.PagingData | ||||
| import dev.adriankuta.pixabay.data.model.PixabayImage | ||||
| import kotlinx.coroutines.flow.Flow | ||||
|  | ||||
| interface ImageRepository { | ||||
|  | ||||
|     suspend fun searchImageById(id: Int): PixabayImage | ||||
|  | ||||
|     suspend fun searchImages(query: String, page: Int, pageSize: Int): List<PixabayImage> | ||||
|  | ||||
|     fun getSearchResultStream(query: String): Flow<PagingData<PixabayImage>> | ||||
| } | ||||
| @@ -0,0 +1,80 @@ | ||||
| @file:OptIn(ExperimentalPagingApi::class) | ||||
|  | ||||
| package dev.adriankuta.pixabay.data.repository | ||||
|  | ||||
| import androidx.paging.ExperimentalPagingApi | ||||
| import androidx.paging.Pager | ||||
| import androidx.paging.PagingConfig | ||||
| import androidx.paging.PagingData | ||||
| import androidx.paging.map | ||||
| import dev.adriankuta.pixabay.data.model.PixabayImage | ||||
| import dev.adriankuta.pixabay.data.network.PixabayService | ||||
| import dev.adriankuta.pixabay.data.paging.PixabayPagingSource | ||||
| import dev.adriankuta.pixabay.data.paging.PixabayRemoteMediator | ||||
| import dev.adriankuta.pixabay.data.room.AppDatabase | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
|  | ||||
| internal class PixabayImageRepository @Inject constructor( | ||||
|     private val database: AppDatabase, | ||||
|     private val pixabayService: PixabayService | ||||
| ) : ImageRepository { | ||||
|  | ||||
|     override suspend fun searchImageById(id: Int): PixabayImage { | ||||
|         return runCatching { | ||||
|             pixabayService.searchImageById(id.toString()) | ||||
|         }.mapCatching { response -> | ||||
|             PixabayImage(response.hits.first()) | ||||
|         }.onFailure { e -> | ||||
|             Timber.e(e) | ||||
|         }.getOrThrow() | ||||
|     } | ||||
|  | ||||
|     override suspend fun searchImages(query: String, page: Int, pageSize: Int): List<PixabayImage> { | ||||
|         return runCatching { | ||||
|             pixabayService.searchImages(query, page, pageSize) | ||||
|         }.mapCatching { response -> | ||||
|             response.hits.map { PixabayImage(it) } | ||||
|         }.onFailure { e -> | ||||
|             Timber.e(e) | ||||
|         }.getOrThrow() | ||||
|     } | ||||
|  | ||||
|     override fun getSearchResultStream(query: String): Flow<PagingData<PixabayImage>> { | ||||
|         return if (USE_CACHE_PAGER) | ||||
|             getOnlinePagerWithCache(query) | ||||
|         else | ||||
|             getOnlinePager(query) | ||||
|     } | ||||
|  | ||||
|     private fun getOnlinePager(query: String) = Pager( | ||||
|         config = PagingConfig( | ||||
|             pageSize = NETWORK_PAGE_SIZE, | ||||
|             enablePlaceholders = false | ||||
|         ), | ||||
|         pagingSourceFactory = { PixabayPagingSource(pixabayService, query) } | ||||
|     ).flow | ||||
|  | ||||
|     private fun getOnlinePagerWithCache(query: String) = Pager( | ||||
|         config = PagingConfig( | ||||
|             pageSize = NETWORK_PAGE_SIZE, | ||||
|             enablePlaceholders = false | ||||
|         ), | ||||
|         remoteMediator = PixabayRemoteMediator( | ||||
|             query, | ||||
|             database, | ||||
|             pixabayService | ||||
|         ), | ||||
|         pagingSourceFactory = { database.imagesDao().imagesByQuery(query) } | ||||
|     ).flow | ||||
|         .map { pagingSource -> | ||||
|             pagingSource.map { PixabayImage(it) } | ||||
|         } | ||||
|  | ||||
|     companion object { | ||||
|         const val NETWORK_PAGE_SIZE = 30 | ||||
|         const val USE_CACHE_PAGER = false | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,19 @@ | ||||
| package dev.adriankuta.pixabay.data.room | ||||
|  | ||||
| import androidx.room.Database | ||||
| import androidx.room.RoomDatabase | ||||
| import dev.adriankuta.pixabay.data.room.dao.PixabayImagesDao | ||||
| import dev.adriankuta.pixabay.data.room.dao.RemoteKeysDao | ||||
| import dev.adriankuta.pixabay.data.room.entity.PixabayImageEntity | ||||
| import dev.adriankuta.pixabay.data.room.entity.RemoteKeys | ||||
|  | ||||
| @Database( | ||||
|     entities = [ | ||||
|         PixabayImageEntity::class, | ||||
|         RemoteKeys::class | ||||
|     ], version = 1 | ||||
| ) | ||||
| internal abstract class AppDatabase : RoomDatabase() { | ||||
|     abstract fun imagesDao(): PixabayImagesDao | ||||
|     abstract fun remoteKeysDao(): RemoteKeysDao | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| package dev.adriankuta.pixabay.data.room.dao | ||||
|  | ||||
| import androidx.paging.PagingSource | ||||
| import androidx.room.Dao | ||||
| import androidx.room.Insert | ||||
| import androidx.room.OnConflictStrategy | ||||
| import androidx.room.Query | ||||
| import dev.adriankuta.pixabay.data.room.entity.PixabayImageEntity | ||||
| import dev.adriankuta.pixabay.data.room.entity.PixabayImageEntity.Companion.COLUMN_QUERY_USED | ||||
| import dev.adriankuta.pixabay.data.room.entity.PixabayImageEntity.Companion.TABLE_NAME | ||||
|  | ||||
| @Dao | ||||
| internal interface PixabayImagesDao { | ||||
|  | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     suspend fun insertAll(pixabayImages: List<PixabayImageEntity>) | ||||
|  | ||||
|  | ||||
|     @Query("SELECT * FROM $TABLE_NAME WHERE $COLUMN_QUERY_USED LIKE :query ORDER BY entry_id ASC") | ||||
|     fun imagesByQuery(query: String): PagingSource<Int, PixabayImageEntity> | ||||
|  | ||||
|  | ||||
|     @Query("DELETE FROM $TABLE_NAME") | ||||
|     suspend fun clearAll() | ||||
| } | ||||
| @@ -0,0 +1,22 @@ | ||||
| package dev.adriankuta.pixabay.data.room.dao | ||||
|  | ||||
| import androidx.room.Dao | ||||
| import androidx.room.Insert | ||||
| import androidx.room.OnConflictStrategy | ||||
| import androidx.room.Query | ||||
| import dev.adriankuta.pixabay.data.room.entity.RemoteKeys | ||||
| import dev.adriankuta.pixabay.data.room.entity.RemoteKeys.Companion.COLUMN_ID | ||||
| import dev.adriankuta.pixabay.data.room.entity.RemoteKeys.Companion.TABLE_NAME | ||||
|  | ||||
| @Dao | ||||
| internal interface RemoteKeysDao { | ||||
|  | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     suspend fun insertAll(remoteKey: List<RemoteKeys>) | ||||
|  | ||||
|     @Query("SELECT * FROM $TABLE_NAME WHERE $COLUMN_ID = :id") | ||||
|     suspend fun remoteKeysImageId(id: Int): RemoteKeys? | ||||
|  | ||||
|     @Query("DELETE FROM $TABLE_NAME") | ||||
|     suspend fun clearRemoteKeys() | ||||
| } | ||||
| @@ -0,0 +1,89 @@ | ||||
| package dev.adriankuta.pixabay.data.room.entity | ||||
|  | ||||
| import androidx.room.ColumnInfo | ||||
| import androidx.room.Entity | ||||
| import androidx.room.PrimaryKey | ||||
| import dev.adriankuta.pixabay.data.dto.response.PixabayImageResponse | ||||
| import dev.adriankuta.pixabay.data.room.entity.PixabayImageEntity.Companion.TABLE_NAME | ||||
|  | ||||
| @Entity(tableName = TABLE_NAME) | ||||
| internal data class PixabayImageEntity( | ||||
|     @PrimaryKey(autoGenerate = true) | ||||
|     @ColumnInfo("entry_id") val entryId: Int = 0, | ||||
|     @ColumnInfo(COLUMN_ID) val id: Int, | ||||
|     @ColumnInfo(COLUMN_QUERY_USED) val queryUsed: String, | ||||
|     @ColumnInfo(COLUMN_PAGE_URL) val pageURL: String, | ||||
|     @ColumnInfo(COLUMN_TYPE) val type: String, | ||||
|     @ColumnInfo(COLUMN_TAGS) val tags: String, | ||||
|     @ColumnInfo(COLUMN_PREVIEW_URL) val previewURL: String, | ||||
|     @ColumnInfo(COLUMN_PREVIEW_WIDTH) val previewWidth: Int, | ||||
|     @ColumnInfo(COLUMN_PREVIEW_HEIGHT) val previewHeight: Int, | ||||
|     @ColumnInfo(COLUMN_WEB_FORMAT_URL) val webformatURL: String, | ||||
|     @ColumnInfo(COLUMN_WEB_FORMAT_WIDTH) val webformatWidth: Int, | ||||
|     @ColumnInfo(COLUMN_WEB_FORMAT_HEIGHT) val webformatHeight: Int, | ||||
|     @ColumnInfo(COLUMN_LARGE_IMAGE_URL) val largeImageURL: String, | ||||
|     @ColumnInfo(COLUMN_IMAGE_WIDTH) val imageWidth: Int, | ||||
|     @ColumnInfo(COLUMN_IMAGE_HEIGHT) val imageHeight: Int, | ||||
|     @ColumnInfo(COLUMN_IMAGE_SIZE) val imageSize: Int, | ||||
|     @ColumnInfo(COLUMN_VIEWS) val views: Int, | ||||
|     @ColumnInfo(COLUMN_DOWNLOADS) val downloads: Int, | ||||
|     @ColumnInfo(COLUMN_LIKES) val likes: Int, | ||||
|     @ColumnInfo(COLUMN_COMMENTS) val comments: Int, | ||||
|     @ColumnInfo(COLUMN_USER_ID) val userId: Int, | ||||
|     @ColumnInfo(COLUMN_USER) val user: String, | ||||
|     @ColumnInfo(COLUMN_USER_IMAGE_URL) val userImageURL: String, | ||||
|  | ||||
| ) { | ||||
|     constructor(pixabayImageResponse: PixabayImageResponse, queryUsed: String) : | ||||
|             this( | ||||
|                 id = pixabayImageResponse.id, | ||||
|                 queryUsed = queryUsed, | ||||
|                 pageURL = pixabayImageResponse.pageURL, | ||||
|                 type = pixabayImageResponse.type, | ||||
|                 tags = pixabayImageResponse.tags, | ||||
|                 previewURL = pixabayImageResponse.previewURL, | ||||
|                 previewWidth = pixabayImageResponse.previewWidth, | ||||
|                 previewHeight = pixabayImageResponse.previewHeight, | ||||
|                 webformatURL = pixabayImageResponse.webformatURL, | ||||
|                 webformatWidth = pixabayImageResponse.webformatWidth, | ||||
|                 webformatHeight = pixabayImageResponse.webformatHeight, | ||||
|                 largeImageURL = pixabayImageResponse.largeImageURL, | ||||
|                 imageWidth = pixabayImageResponse.imageWidth, | ||||
|                 imageHeight = pixabayImageResponse.imageHeight, | ||||
|                 imageSize = pixabayImageResponse.imageSize, | ||||
|                 views = pixabayImageResponse.views, | ||||
|                 downloads = pixabayImageResponse.downloads, | ||||
|                 likes = pixabayImageResponse.likes, | ||||
|                 comments = pixabayImageResponse.comments, | ||||
|                 userId = pixabayImageResponse.user_id, | ||||
|                 user = pixabayImageResponse.user, | ||||
|                 userImageURL = pixabayImageResponse.userImageURL | ||||
|             ) | ||||
|  | ||||
|  | ||||
|     companion object { | ||||
|         const val TABLE_NAME = "pixabay_images" | ||||
|         const val COLUMN_ID = "id" | ||||
|         const val COLUMN_QUERY_USED = "query_used" | ||||
|         const val COLUMN_PAGE_URL = "page_url" | ||||
|         const val COLUMN_TYPE = "type" | ||||
|         const val COLUMN_TAGS = "tags" | ||||
|         const val COLUMN_PREVIEW_URL = "preview_url" | ||||
|         const val COLUMN_PREVIEW_WIDTH = "preview_width" | ||||
|         const val COLUMN_PREVIEW_HEIGHT = "preview_height" | ||||
|         const val COLUMN_WEB_FORMAT_URL = "web_format_url" | ||||
|         const val COLUMN_WEB_FORMAT_WIDTH = "web_format_width" | ||||
|         const val COLUMN_WEB_FORMAT_HEIGHT = "web_format_height" | ||||
|         const val COLUMN_LARGE_IMAGE_URL = "large_image_url" | ||||
|         const val COLUMN_IMAGE_WIDTH = "image_width" | ||||
|         const val COLUMN_IMAGE_HEIGHT = "image_height" | ||||
|         const val COLUMN_IMAGE_SIZE = "image_size" | ||||
|         const val COLUMN_VIEWS = "views" | ||||
|         const val COLUMN_DOWNLOADS = "downloads" | ||||
|         const val COLUMN_LIKES = "likes" | ||||
|         const val COLUMN_COMMENTS = "comments" | ||||
|         const val COLUMN_USER_ID = "user_id" | ||||
|         const val COLUMN_USER = "user" | ||||
|         const val COLUMN_USER_IMAGE_URL = "user_image_url" | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| package dev.adriankuta.pixabay.data.room.entity | ||||
|  | ||||
| import androidx.room.ColumnInfo | ||||
| import androidx.room.Entity | ||||
| import androidx.room.PrimaryKey | ||||
| import dev.adriankuta.pixabay.data.room.entity.RemoteKeys.Companion.TABLE_NAME | ||||
|  | ||||
| @Entity(tableName = TABLE_NAME) | ||||
| internal data class RemoteKeys( | ||||
|     @PrimaryKey | ||||
|     @ColumnInfo(name = COLUMN_ID) | ||||
|     val id: Int, | ||||
|     @ColumnInfo(name = COLUMN_PREV_KEY) | ||||
|     val prevKey: Int?, | ||||
|     @ColumnInfo(name = COLUMN_NEXT_KEY) | ||||
|     val nextKey: Int? | ||||
| ) { | ||||
|     companion object { | ||||
|         const val TABLE_NAME = "remote_keys" | ||||
|         const val COLUMN_ID = "id" | ||||
|         const val COLUMN_PREV_KEY = "prev_key" | ||||
|         const val COLUMN_NEXT_KEY = "next_key" | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user