Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
plugins {
id("com.android.application")
id("dagger.hilt.android.plugin")
kotlin("android")
kotlin("android.extensions")
kotlin("kapt")
}

android {
Expand All @@ -23,6 +25,16 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
kapt {
correctErrorTypes = true
}
}

dependencies {
Expand All @@ -31,6 +43,39 @@ dependencies {
implementation(Libs.coreKtx)
implementation(Libs.appCompat)
implementation(Libs.constraintLayout)

// dagger
implementation(Libs.daggerHilt)
implementation(Libs.daggerHiltKtx)
kapt(Libs.daggerKapt)
kapt(Libs.daggerKaptKtx)

// ktx
implementation(Libs.viewModelKtx)
implementation(Libs.fragmentKtx)

// paging
implementation(Libs.paging3)

// room
implementation(Libs.roomKtx)
implementation(Libs.roomDatabase)
kapt(Libs.roomKapt)

// retrofit
implementation(Libs.retrofit)
implementation(Libs.jsonConverter)

// coroutine
implementation(Libs.coroutineCore)
implementation(Libs.coroutineAndroid)

// logging okhttp3
implementation(Libs.loggingInterceptor)

// picasso
implementation(Libs.picasso)

testImplementation(TestLibs.junit)
androidTestImplementation(TestLibs.extJunit)
androidTestImplementation(TestLibs.espressoCore)
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="gst.trainingcourse.sampleproject">

<uses-permission android:name="android.permission.INTERNET"/>

<application
android:name=".AppApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<activity android:name=".ui.main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gst.trainingcourse.sampleproject

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class AppApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}
11 changes: 0 additions & 11 deletions app/src/main/java/gst/trainingcourse/sampleproject/MainActivity.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package gst.trainingcourse.sampleproject.data.datasource.local

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import gst.trainingcourse.sampleproject.data.datasource.local.CatImageDatabase.Companion.DATABASE_VERSION
import gst.trainingcourse.sampleproject.data.datasource.local.dao.CatImageDAO
import gst.trainingcourse.sampleproject.data.datasource.local.dao.RemoteKeysDAO
import gst.trainingcourse.sampleproject.data.model.CatImageEntity
import gst.trainingcourse.sampleproject.data.model.RemoteKey

@Database(
entities = [CatImageEntity::class, RemoteKey::class],
version = DATABASE_VERSION,
exportSchema = false
)
abstract class CatImageDatabase : RoomDatabase() {
abstract fun catImageDAO(): CatImageDAO
abstract fun remoteKeyDAO(): RemoteKeysDAO

companion object {
const val DATABASE_VERSION = 1
const val DATABASE_NAME = "cat_image_db"
private var INSTANCE: CatImageDatabase? = null
fun getInstance(context: Context) = INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context,
CatImageDatabase::class.java,
DATABASE_NAME
).allowMainThreadQueries().build().also { INSTANCE = it }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gst.trainingcourse.sampleproject.data.datasource.local

import android.util.Log
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import gst.trainingcourse.sampleproject.data.datasource.remote.CatImageApi
import gst.trainingcourse.sampleproject.data.datasource.remote.CatImageResponse
import gst.trainingcourse.sampleproject.data.model.CatImageEntity
import gst.trainingcourse.sampleproject.domain.Mapper
import gst.trainingcourse.sampleproject.domain.model.CatImage
import gst.trainingcourse.sampleproject.domain.repository.CatImageRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class CatImageLocalDataSource @Inject constructor(
private val catImageApi: CatImageApi,
private val database: CatImageDatabase,
private val apiToEntity: Mapper<CatImageResponse, CatImageEntity>,
private val entityToDomain: Mapper<PagingData<CatImageEntity>, PagingData<CatImage>>
) : CatImageRepository {
override fun getCatImages(): Flow<PagingData<CatImage>> {
Log.d("MyLogTag", "class: CatImageLocalDataSource func: getCatImages: (23) ")
val cat = CatImageRemoteMediator(database, catImageApi, apiToEntity)
return Pager(
config = PagingConfig(20),
remoteMediator = CatImageRemoteMediator(database, catImageApi, apiToEntity)
) {
database.catImageDAO().getCatImages()
}.flow.map {
it.run(entityToDomain)
}
// return data.map {
// it.run(entityToDomain)
// }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package gst.trainingcourse.sampleproject.data.datasource.local

import android.util.Log
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import gst.trainingcourse.sampleproject.data.datasource.remote.CatImageApi
import gst.trainingcourse.sampleproject.data.datasource.remote.CatImageResponse
import gst.trainingcourse.sampleproject.data.model.CatImageEntity
import gst.trainingcourse.sampleproject.data.model.RemoteKey
import gst.trainingcourse.sampleproject.domain.Mapper
import java.io.IOException
import java.io.InvalidObjectException
import javax.inject.Inject

@OptIn(ExperimentalPagingApi::class)
class CatImageRemoteMediator @Inject constructor(
private val database: CatImageDatabase,
private val catImageApi: CatImageApi,
private val apiToEntity: Mapper<CatImageResponse, CatImageEntity>
) : RemoteMediator<Int, CatImageEntity>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, CatImageEntity>
): MediatorResult {
Log.d("MyLogTag", "class: CatImageRemoteMediator func: load: (22) $loadType")
val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKey = getRemoteKeyClosetToCurrentPosition(state)
remoteKey?.nextKey?.minus(1) ?: 1
}
LoadType.PREPEND -> {
return MediatorResult.Success(true)
}
LoadType.APPEND -> {
val remoteKey = getRemoteKeyForLastItem(state)
if (remoteKey?.nextKey == null) {
throw InvalidObjectException("Remote key should not be null for $loadType")
}
remoteKey.nextKey
}
}
Log.d("MyLogTag", "class: CatImageRemoteMediator func: load: (40) $page")
try {
val data = catImageApi.getCatImages(page, state.config.pageSize).map(apiToEntity)
val endOfPaginationReached = data.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
with(database) {
remoteKeyDAO().clearRemoteKeys()
catImageDAO().clearCatImages()
}
}
val prevKey = if (page == 1) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = data.map {
RemoteKey(id = it.id, prevKey = prevKey, nextKey = nextKey)
}
Log.d("MyLogTag", "class: CatImageRemoteMediator func: load: (61) $keys")
database.remoteKeyDAO().insertAll(keys)
database.catImageDAO().insertAll(data)
}
return MediatorResult.Success(endOfPaginationReached = true)
} catch (e: IOException) {
return MediatorResult.Error(e)
}
}

private suspend fun getRemoteKeyClosetToCurrentPosition(state: PagingState<Int, CatImageEntity>): RemoteKey? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.id?.let {
database.remoteKeyDAO().getRemoteKeyById(it)
}
}.also {
Log.d(
"MyLogTag",
"class: CatImageRemoteMediator func: getRemoteKeyClosetToCurrentPosition: (65) $it"
)
}
}

private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, CatImageEntity>): RemoteKey? {
return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()?.let {
database.remoteKeyDAO().getRemoteKeyById(it.id)
}.also {
Log.d(
"MyLogTag",
"class: CatImageRemoteMediator func: getRemoteKeyForLastItem: (76) $it"
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package gst.trainingcourse.sampleproject.data.datasource.local.dao

import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import gst.trainingcourse.sampleproject.data.model.CatImageEntity

@Dao
interface CatImageDAO {
@Query("SELECT * FROM tb_cat_image")
fun getCatImages(): PagingSource<Int, CatImageEntity>

@Insert
fun insertAll(catImage: List<CatImageEntity>)

@Query("DELETE FROM tb_cat_image")
suspend fun clearCatImages()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package gst.trainingcourse.sampleproject.data.datasource.local.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import gst.trainingcourse.sampleproject.data.model.RemoteKey

@Dao
interface RemoteKeysDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(remoteKey: List<RemoteKey>)

@Query("SELECT * FROM remotekey WHERE id = :id")
suspend fun getRemoteKeyById(id: String): RemoteKey

@Query("SELECT * FROM remotekey")
suspend fun getRemoteKeys(): List<RemoteKey>

@Query("DELETE FROM remotekey")
suspend fun clearRemoteKeys()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package gst.trainingcourse.sampleproject.data.datasource.mapper

import androidx.paging.PagingData
import androidx.paging.map
import gst.trainingcourse.sampleproject.data.model.CatImageEntity
import gst.trainingcourse.sampleproject.domain.Mapper
import gst.trainingcourse.sampleproject.domain.model.CatImage

class CatImageEntityToDomain : Mapper<PagingData<CatImageEntity>, PagingData<CatImage>> {
override fun invoke(paging: PagingData<CatImageEntity>): PagingData<CatImage> {
return paging.map {
CatImage(id = it.id, url = it.url)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package gst.trainingcourse.sampleproject.data.datasource.mapper

import gst.trainingcourse.sampleproject.data.datasource.remote.CatImageResponse
import gst.trainingcourse.sampleproject.data.model.CatImageEntity
import gst.trainingcourse.sampleproject.domain.Mapper

class CatImageResponseToCatImageEntity : Mapper<CatImageResponse, CatImageEntity> {
override fun invoke(input: CatImageResponse): CatImageEntity {
return CatImageEntity(
id = input.id,
url = input.url
)
}
}
Loading