Skip to content

Commit

Permalink
[1.1.0/AN_FEAT] 배틀 선택 데이터 저장 (#324)
Browse files Browse the repository at this point in the history
* chore: DataStore 의존성 설정

* feat: Datastore 저장 로직 추가

* feat: 저장된 선택 데이터 UI 에 반영

* refactor: Skill 캐싱

* refactor: Test 객체 수정

* refactor: context 참조 수정

* refactor: 리팩토링 리뷰 반영

* refactor: 아이디 저장값 분리

* refactor: Map 형태 변경
  • Loading branch information
JoYehyun99 authored Sep 26, 2024
1 parent 4c0fd35 commit 59efa72
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import poke.rogue.helper.presentation.util.view.setImage

class BattleActivity : ToolbarActivity<ActivityBattleBinding>(R.layout.activity_battle) {
private val viewModel by viewModels<BattleViewModel> {
BattleViewModel.factory(DefaultBattleRepository.instance())
BattleViewModel.factory(DefaultBattleRepository.instance(this))
}
private val weatherAdapter by lazy {
WeatherSpinnerAdapter(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ data class BattleSelectionsState(
) {
val allSelected: Boolean
get() =
minePokemon.isSelected() && skill.isSelected() && opponentPokemon.isSelected()
listOf(minePokemon, skill, opponentPokemon, weather).all { it.isSelected() }

companion object {
val DEFAULT =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
Expand All @@ -23,6 +24,7 @@ import poke.rogue.helper.presentation.battle.model.SelectionData
import poke.rogue.helper.presentation.battle.model.SelectionMode
import poke.rogue.helper.presentation.battle.model.SkillSelectionUiModel
import poke.rogue.helper.presentation.battle.model.WeatherUiModel
import poke.rogue.helper.presentation.battle.model.toSelectionUi
import poke.rogue.helper.presentation.battle.model.toUi

class BattleViewModel(
Expand Down Expand Up @@ -58,19 +60,23 @@ class BattleViewModel(

init {
initWeathers()
initSavedSelection()
}

private suspend fun fetchBattlePredictionResult(): BattlePredictionUiModel {
with(selectedState.value) {
val weatherId = weather.selectedData()?.id
val myPokemonId = minePokemon.selectedData()?.id
val mySkillId = skill.selectedData()?.id
val opponentPokemonId = opponentPokemon.selectedData()?.id
val weatherId = requireNotNull(weather.selectedData()?.id) { "날씨는 null일 수 없습니다." }
val myPokemonId =
requireNotNull(minePokemon.selectedData()?.id) { "내 포켓몬은 null일 수 없습니다." }
val mySkillId = requireNotNull(skill.selectedData()?.id) { "내 스킬은 null일 수 없습니다." }
val opponentPokemonId =
requireNotNull(opponentPokemon.selectedData()?.id) { "상대 포켓몬은 null일 수 없습니다." }

return battleRepository.calculatedBattlePrediction(
weatherId = "$weatherId",
myPokemonId = "$myPokemonId",
mySkillId = "$mySkillId",
opponentPokemonId = "$opponentPokemonId",
weatherId = weatherId,
myPokemonId = myPokemonId,
mySkillId = mySkillId,
opponentPokemonId = opponentPokemonId,
).toUi()
}
}
Expand All @@ -82,6 +88,21 @@ class BattleViewModel(
}
}

private fun initSavedSelection() {
viewModelScope.launch {
launch {
battleRepository.savedPokemon().first()?.let {
updateOpponentPokemon(it.toSelectionUi())
}
}
launch {
battleRepository.savedPokemonWithSkill().first()?.let { (pokemon, skill) ->
updateMyPokemon(pokemon.toSelectionUi(), skill.toUi())
}
}
}
}

fun updateWeather(newWeather: WeatherUiModel) {
viewModelScope.launch {
val selectedWeather = BattleSelectionUiState.Selected(newWeather)
Expand All @@ -91,19 +112,24 @@ class BattleViewModel(

fun updatePokemonSelection(selection: SelectionData) {
when (selection) {
is SelectionData.WithSkill ->
{
updateMyPokemon(
selection.selectedPokemon,
selection.selectedSkill,
)
logger.logPokemonSkillSelection(selection)
is SelectionData.WithSkill -> {
val (selectedPokemon, selectedSkill) = selection
updateMyPokemon(selectedPokemon, selectedSkill)
viewModelScope.launch {
battleRepository.savePokemonWithSkill(selectedPokemon.id, selectedSkill.id)
}
logger.logPokemonSkillSelection(selection)
}

is SelectionData.WithoutSkill -> {
updateOpponentPokemon(selection.selectedPokemon)
val selectedPokemon = selection.selectedPokemon
updateOpponentPokemon(selectedPokemon)
viewModelScope.launch {
battleRepository.savePokemon(selectedPokemon.id)
}
logger.logBattlePokemonSelection(selection)
}

is SelectionData.NoSelection -> {}
}
}
Expand All @@ -112,19 +138,15 @@ class BattleViewModel(
pokemon: PokemonSelectionUiModel,
skill: SkillSelectionUiModel,
) {
viewModelScope.launch {
val selectedPokemon = BattleSelectionUiState.Selected(pokemon)
val selectedSkill = BattleSelectionUiState.Selected(skill)
_selectedState.value =
selectedState.value.copy(minePokemon = selectedPokemon, skill = selectedSkill)
}
val selectedPokemon = BattleSelectionUiState.Selected(pokemon)
val selectedSkill = BattleSelectionUiState.Selected(skill)
_selectedState.value =
selectedState.value.copy(minePokemon = selectedPokemon, skill = selectedSkill)
}

private fun updateOpponentPokemon(pokemon: PokemonSelectionUiModel) {
viewModelScope.launch {
val selectedPokemon = BattleSelectionUiState.Selected(pokemon)
_selectedState.value = selectedState.value.copy(opponentPokemon = selectedPokemon)
}
val selectedPokemon = BattleSelectionUiState.Selected(pokemon)
_selectedState.value = selectedState.value.copy(opponentPokemon = selectedPokemon)
}

override fun navigateToSelection(selectionMode: SelectionMode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class SkillSelectionFragment :
private val sharedViewModel: BattleSelectionViewModel by activityViewModels()
private val viewModel: SkillSelectionViewModel by viewModels<SkillSelectionViewModel> {
SkillSelectionViewModel.factory(
DefaultBattleRepository.instance(),
DefaultBattleRepository.instance(requireContext().applicationContext),
sharedViewModel.previousSelection as? SelectionData.WithSkill,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package poke.rogue.helper.data.datasource

import android.content.Context
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import poke.rogue.helper.data.model.PokemonWithSkillIds
import poke.rogue.helper.data.model.toData
import poke.rogue.helper.local.datastore.BattleDataStore

class LocalBattleDataSource(private val battleDataStore: BattleDataStore) {
suspend fun savePokemonWithSkill(
pokemonId: String,
skillId: String,
) {
battleDataStore.savePokemonWithSkill(pokemonId, skillId)
}

suspend fun savePokemon(pokemonId: String) {
battleDataStore.savePokemon(pokemonId)
}

fun pokemonWithSkill(): Flow<PokemonWithSkillIds?> = battleDataStore.pokemonWithSkillId().map { it?.toData() }

fun pokemonId(): Flow<String?> = battleDataStore.pokemonId()

companion object {
private var instance: LocalBattleDataSource? = null

fun instance(context: Context): LocalBattleDataSource {
return instance ?: LocalBattleDataSource(
BattleDataStore(context),
).also {
instance = it
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package poke.rogue.helper.data.model

import poke.rogue.helper.local.datastore.SavedPokemonWithSkill

data class PokemonWithSkillIds(val pokemonId: String, val skillId: String)

fun SavedPokemonWithSkill.toData() = PokemonWithSkillIds(pokemonId, skillId)

data class PokemonWithSkill(val pokemon: Pokemon, val skill: BattleSkill)
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package poke.rogue.helper.data.repository

import kotlinx.coroutines.flow.Flow
import poke.rogue.helper.data.model.BattlePrediction
import poke.rogue.helper.data.model.BattleSkill
import poke.rogue.helper.data.model.Pokemon
import poke.rogue.helper.data.model.PokemonWithSkill
import poke.rogue.helper.data.model.Weather

interface BattleRepository {
Expand All @@ -15,4 +18,15 @@ interface BattleRepository {
mySkillId: String,
opponentPokemonId: String,
): BattlePrediction

suspend fun savePokemon(pokemonId: String)

suspend fun savePokemonWithSkill(
pokemonId: String,
skillId: String,
)

suspend fun savedPokemon(): Flow<Pokemon?>

suspend fun savedPokemonWithSkill(): Flow<PokemonWithSkill?>
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
package poke.rogue.helper.data.repository

import android.content.Context
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import poke.rogue.helper.data.datasource.LocalBattleDataSource
import poke.rogue.helper.data.datasource.RemoteBattleDataSource
import poke.rogue.helper.data.model.BattlePrediction
import poke.rogue.helper.data.model.BattleSkill
import poke.rogue.helper.data.model.Pokemon
import poke.rogue.helper.data.model.PokemonWithSkill
import poke.rogue.helper.data.model.Weather

class DefaultBattleRepository(private val remoteBattleDataSource: RemoteBattleDataSource) : BattleRepository {
class DefaultBattleRepository(
private val localBattleDataSource: LocalBattleDataSource,
private val remoteBattleDataSource: RemoteBattleDataSource,
private val pokemonRepository: DexRepository,
) : BattleRepository {
private val cachedSkills: MutableMap<Long, List<BattleSkill>> = mutableMapOf()

override suspend fun weathers(): List<Weather> = remoteBattleDataSource.weathers()

override suspend fun availableSkills(dexNumber: Long): List<BattleSkill> = remoteBattleDataSource.availableSkills(dexNumber).distinct()
override suspend fun availableSkills(dexNumber: Long): List<BattleSkill> =
cachedSkills[dexNumber] ?: run {
val skills = remoteBattleDataSource.availableSkills(dexNumber).distinct()
cachedSkills[dexNumber] = skills
skills
}

override suspend fun calculatedBattlePrediction(
weatherId: String,
Expand All @@ -23,11 +40,46 @@ class DefaultBattleRepository(private val remoteBattleDataSource: RemoteBattleDa
opponentPokemonId = opponentPokemonId,
)

override suspend fun savePokemon(pokemonId: String) {
localBattleDataSource.savePokemon(pokemonId)
}

override suspend fun savePokemonWithSkill(
pokemonId: String,
skillId: String,
) {
localBattleDataSource.savePokemonWithSkill(pokemonId, skillId)
}

override suspend fun savedPokemon(): Flow<Pokemon?> =
localBattleDataSource.pokemonId().map { it?.let { pokemonRepository.pokemon(it) } }

override suspend fun savedPokemonWithSkill(): Flow<PokemonWithSkill?> =
localBattleDataSource.pokemonWithSkill().map {
it?.let { pokemonWithSkill ->
val pokemon = pokemonRepository.pokemon(pokemonWithSkill.pokemonId)
val skill = skill(pokemon.dexNumber, pokemonWithSkill.skillId)
PokemonWithSkill(pokemon, skill)
}
}

private suspend fun skill(
dexNumber: Long,
skillId: String,
): BattleSkill =
availableSkills(dexNumber).find {
it.id == skillId
} ?: error("아이디에 해당하는 스킬이 존재하지 않습니다. id: $skillId")

companion object {
private var instance: BattleRepository? = null

fun instance(): BattleRepository {
return instance ?: DefaultBattleRepository(RemoteBattleDataSource.instance()).also {
fun instance(context: Context): BattleRepository {
return instance ?: DefaultBattleRepository(
LocalBattleDataSource.instance(context),
RemoteBattleDataSource.instance(),
DefaultDexRepository.instance(),
).also {
instance = it
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ class DefaultDexRepository(
}
}

override suspend fun pokemon(id: String): Pokemon {
cachedPokemons.find { it.id == id }?.let {
return it
}
return pokemons().find { it.id == id } ?: error("아이디에 해당하는 포켓몬이 존재하지 않습니다. id : $id")
}

private suspend fun pokemonDetail(
id: String,
allBiomes: List<Biome>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ interface DexRepository {
): List<Pokemon>

suspend fun pokemonDetail(id: String): PokemonDetail

suspend fun pokemon(id: String): Pokemon
}
2 changes: 2 additions & 0 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ coil = "2.6.0"
glide = "4.14.2"
ktlint = "12.1.0"
splash-screen = "1.0.1"
datastore = "1.0.0"

# Google & Firebase
google-services = "4.4.2"
Expand Down Expand Up @@ -81,6 +82,7 @@ room = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" }
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }

# Google & Firebase
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase" }
Expand Down
3 changes: 3 additions & 0 deletions android/local/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.kotlinx.serialization)
}

android {
Expand Down Expand Up @@ -46,12 +47,14 @@ android {

dependencies {
implementation(libs.kotlin.coroutines.android)
implementation(libs.kotlin.serialization.json)
// third-party
implementation(libs.timber)
// room
implementation(libs.room)
implementation(libs.room.ktx)
kapt(libs.room.compiler)
implementation(libs.datastore.preferences)
// unit test
testImplementation(libs.bundles.unit.test)
testImplementation(libs.kotlin.test)
Expand Down
Empty file.
Loading

0 comments on commit 59efa72

Please sign in to comment.