Skip to content

Commit

Permalink
[1.1.0-AN_FEAT] 스플래시 때 포켓몬 front 이미지 캐시 (#319)
Browse files Browse the repository at this point in the history
* feat: Splash 화면에서 24개 사진 Preload

* [1.1.0/AN_CD] Discord로 alpha 버전 apk 전달 (#290)

* 🎉 Chore: PR_Comment_Notification.yml 

PR Comment 에 '/noti' 를 포함할 경우 Andorid 채널 혹은 Backend 채널로 노티가 간다

* 🎉 Chore: �Avatar, Content, Description 수정

* docs: README v0.1

* CD: discord로 apk 전달

* CD: cd되는지 확인

* ci: artifact 버전 v3로 수정

* test

* build : 병렬처리

* Revert "Merge branch 'main' into an/cd/send_to_discord"

This reverts commit 925c8db, reversing
changes made to 346391c.

* ci: google-service추가

* ci: 버저닝 label 추가

* fix: 매직 넘버 상수화 및 예외처리 문 추가

* fix

* style: ktFormat

* fix: withContext -> coroutineScope, 주석 변경

* fix: Splash 시간 최소 시간 설정
  • Loading branch information
murjune authored Sep 22, 2024
1 parent 1326b1a commit 0bd8290
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package poke.rogue.helper.presentation.splash

import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import poke.rogue.helper.analytics.AnalyticsLogger
import poke.rogue.helper.data.repository.DexRepository
import poke.rogue.helper.presentation.base.BaseViewModelFactory
import poke.rogue.helper.presentation.base.error.ErrorHandleViewModel
import poke.rogue.helper.presentation.util.event.MutableEventFlow
import poke.rogue.helper.presentation.util.event.asEventFlow
import timber.log.Timber

class PokemonIntroViewModel(
private val pokemonRepository: DexRepository,
Expand All @@ -27,17 +27,23 @@ class PokemonIntroViewModel(
refreshEvent
.onStart { emit(Unit) }
.onEach {
coroutineScope {
val warmUp = async { pokemonRepository.warmUp() }
val delay = async { delay(1000) }
listOf(warmUp, delay).awaitAll()
try {
coroutineScope {
launch { delay(MIN_SPLASH_TIME) }
launch { pokemonRepository.warmUp() }
}
} catch (e: Exception) {
Timber.e(e)
} finally {
_navigationToHomeEvent.emit(Unit)
}
_navigationToHomeEvent.emit(Unit)
}
.launchIn(viewModelScope + errorHandler)
}

companion object {
private const val MIN_SPLASH_TIME = 1000L

fun factory(
pokemonRepository: DexRepository,
logger: AnalyticsLogger,
Expand Down
1 change: 1 addition & 0 deletions android/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
implementation(libs.kotlin)
// third-party
implementation(libs.timber)
implementation(libs.glide)
// test
testImplementation(libs.bundles.unit.test)
testImplementation(libs.kotlin.test)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package poke.rogue.helper.data.cache

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import poke.rogue.helper.data.exception.UnknownException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

class GlideImageCacher(private val context: Context) : ImageCacher {
override suspend fun cacheImages(urls: List<String>) =
coroutineScope {
urls.forEach { url ->
launch {
cacheImage(url)
}
}
}

/**
* 백그라운드 작업에서는 submit() 메서드를 사용, 이 경우 RequestListener도 백그라운드 스레드에서 호출
* UI 작업을 할 때는 into() 메서드를 사용, 이 경우 Main 스레드에서 RequestListener 이 호출
*/
private suspend fun cacheImage(url: String) =
suspendCancellableCoroutine<Unit> { con ->
val requestListener =
object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean,
): Boolean {
con.resumeWithException(
e ?: UnknownException(IllegalStateException("이미지 로드 실패 url: $url")),
)
return false
}

override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean,
): Boolean {
con.resume(Unit)
return false
}
}

val target =
Glide.with(context)
.load(url)
.listener(requestListener)
.submit()

con.invokeOnCancellation {
Glide.with(context).clear(target)
}
}

override suspend fun clear() {
Glide.get(context).clearMemory()
}

companion object {
@SuppressLint("StaticFieldLeak")
private var instance: GlideImageCacher? = null

fun init(context: Context) {
require(instance == null) {
"GlideImageCacher is already initialized"
}
instance = GlideImageCacher(context)
}

fun instance(): GlideImageCacher {
return requireNotNull(instance) {
"GlideImageCacher is not initialized"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package poke.rogue.helper.data.cache

interface ImageCacher {
suspend fun cacheImages(urls: List<String>)

suspend fun clear()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package poke.rogue.helper.data.repository

import android.content.Context
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import poke.rogue.helper.analytics.AnalyticsLogger
import poke.rogue.helper.analytics.analyticsLogger
import poke.rogue.helper.data.cache.GlideImageCacher
import poke.rogue.helper.data.cache.ImageCacher
import poke.rogue.helper.data.datasource.LocalDexDataSource
import poke.rogue.helper.data.datasource.RemoteDexDataSource
import poke.rogue.helper.data.model.Biome
Expand All @@ -18,18 +21,31 @@ import poke.rogue.helper.stringmatcher.has
class DefaultDexRepository(
private val remotePokemonDataSource: RemoteDexDataSource,
private val localPokemonDataSource: LocalDexDataSource,
private val imageCacher: ImageCacher,
private val biomeRepository: BiomeRepository,
private val analyticsLogger: AnalyticsLogger,
) : DexRepository {
private var cachedPokemons: List<Pokemon> = emptyList()

override suspend fun warmUp() {
if (localPokemonDataSource.pokemons().isEmpty()) {
localPokemonDataSource.savePokemons(remotePokemonDataSource.pokemons2())
val pokemons = remotePokemonDataSource.pokemons2()
cachePokemonData(pokemons)
}
cachedPokemons = localPokemonDataSource.pokemons()
}

private suspend fun cachePokemonData(pokemons: List<Pokemon>) =
coroutineScope {
val urls = pokemons.take(PLELOAD_POKEMON_COUNT).map { it.imageUrl }
launch {
imageCacher.cacheImages(urls)
}
launch {
localPokemonDataSource.savePokemons(pokemons)
}
}

override suspend fun pokemons(): List<Pokemon> {
if (cachedPokemons.isEmpty()) {
warmUp()
Expand Down Expand Up @@ -93,12 +109,15 @@ class DefaultDexRepository(

companion object {
private var instance: DexRepository? = null
const val PLELOAD_POKEMON_COUNT = 24

fun init(context: Context) {
GlideImageCacher.init(context)
instance =
DefaultDexRepository(
RemoteDexDataSource.instance(),
LocalDexDataSource.instance(context),
GlideImageCacher.instance(),
DefaultBiomeRepository.instance(),
analyticsLogger(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface PokemonDao {

companion object {
fun instance(context: Context): PokemonDao {
PokeRogueDatabase.dropDatabase(context)
return PokeRogueDatabase.instance(context).pokemonDao()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,12 @@ abstract class PokeRogueDatabase : RoomDatabase() {
).build().also { instance = it }
}
}

fun dropDatabase(context: Context) =
synchronized(this) {
instance?.close()
instance = null
context.deleteDatabase(DATABASE_NAME)
}
}
}

0 comments on commit 0bd8290

Please sign in to comment.