diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 105d65d8..932469a9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -21,7 +21,7 @@ android:name=".presentation.tip.TipActivity" android:exported="false" /> (R.layout.activity_pokemon) { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState == null) { - supportFragmentManager.commit { - replace(R.id.fragment_container_pokemon) - } - } - } -} diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/dex/PokemonListFragment.kt b/android/app/src/main/java/poke/rogue/helper/presentation/dex/PokemonListActivity.kt similarity index 72% rename from android/app/src/main/java/poke/rogue/helper/presentation/dex/PokemonListFragment.kt rename to android/app/src/main/java/poke/rogue/helper/presentation/dex/PokemonListActivity.kt index dcda74bd..552f1cd9 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/dex/PokemonListFragment.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/dex/PokemonListActivity.kt @@ -2,23 +2,22 @@ package poke.rogue.helper.presentation.dex import android.content.res.Configuration import android.os.Bundle -import android.view.View +import androidx.activity.viewModels import androidx.appcompat.widget.Toolbar -import androidx.fragment.app.commit -import androidx.fragment.app.viewModels import androidx.recyclerview.widget.GridLayoutManager import poke.rogue.helper.R import poke.rogue.helper.data.repository.DefaultDexRepository -import poke.rogue.helper.databinding.FragmentPokemonListBinding -import poke.rogue.helper.presentation.base.error.ErrorHandleFragment +import poke.rogue.helper.databinding.ActivityPokemonListBinding +import poke.rogue.helper.presentation.base.error.ErrorHandleActivity import poke.rogue.helper.presentation.base.error.ErrorHandleViewModel import poke.rogue.helper.presentation.dex.detail.PokemonDetailActivity +import poke.rogue.helper.presentation.util.activity.hideKeyboard import poke.rogue.helper.presentation.util.repeatOnStarted import poke.rogue.helper.presentation.util.view.GridSpacingItemDecoration import poke.rogue.helper.presentation.util.view.dp -class PokemonListFragment : - ErrorHandleFragment(R.layout.fragment_pokemon_list) { +class PokemonListActivity : + ErrorHandleActivity(R.layout.activity_pokemon_list) { private val viewModel by viewModels { PokemonListViewModel.factory( DefaultDexRepository.instance(), @@ -34,16 +33,15 @@ class PokemonListFragment : override val toolbar: Toolbar get() = binding.toolbarDex - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) binding.viewModel = viewModel - binding.lifecycleOwner = viewLifecycleOwner - + binding.lifecycleOwner = this initAdapter() initObservers() + binding.root.setOnClickListener { + hideKeyboard() + } } private fun initAdapter() { @@ -70,14 +68,13 @@ class PokemonListFragment : } repeatOnStarted { viewModel.navigateToDetailEvent.collect { pokemonId -> - parentFragmentManager.commit { - startActivity(PokemonDetailActivity.intent(requireContext(), pokemonId)) - } + hideKeyboard() + startActivity(PokemonDetailActivity.intent(this, pokemonId)) } } } companion object { - val TAG: String = PokemonListFragment::class.java.simpleName + val TAG: String = PokemonListActivity::class.java.simpleName } } diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/dex/PokemonListViewModel.kt b/android/app/src/main/java/poke/rogue/helper/presentation/dex/PokemonListViewModel.kt index 0f9f0270..86f1a954 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/dex/PokemonListViewModel.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/dex/PokemonListViewModel.kt @@ -10,18 +10,17 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.plus import poke.rogue.helper.analytics.AnalyticsLogger import poke.rogue.helper.analytics.analyticsLogger +import poke.rogue.helper.data.exception.PokeException import poke.rogue.helper.data.model.Pokemon import poke.rogue.helper.data.repository.DexRepository import poke.rogue.helper.presentation.base.BaseViewModelFactory @@ -35,33 +34,26 @@ class PokemonListViewModel( ) : ErrorHandleViewModel(logger), PokemonListNavigateHandler, PokemonQueryHandler { private val searchQuery = MutableStateFlow("") - private val _isLoading = MutableStateFlow(false) - val isLoading: StateFlow = _isLoading.asStateFlow() - @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) val uiState: StateFlow> = - refreshEvent + merge(refreshEvent.map { "" }, searchQuery) .onStart { - _isLoading.value = true - emit(Unit) + if (isEmpty.value) { + _isLoading.value = true + } } - .flatMapLatest { - searchQuery - .debounce(300L) - .mapLatest { query -> - queriedPokemons(query) - } - .catch { e -> - handlePokemonError(e) - } - }.onEach { - _isLoading.value = false + .debounce(300L) + .mapLatest { query -> + queriedPokemons(query) } .stateIn( viewModelScope + errorHandler, SharingStarted.WhileSubscribed(5000), emptyList(), ) + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + val isEmpty: StateFlow = uiState.map { it.isEmpty() && !isLoading.value } .stateIn( @@ -74,10 +66,18 @@ class PokemonListViewModel( val navigateToDetailEvent = _navigateToDetailEvent.asSharedFlow() private suspend fun queriedPokemons(query: String): List { - if (query.isEmpty()) { - return pokemonListRepository.pokemons().map(Pokemon::toUi) + return try { + if (query.isEmpty()) { + pokemonListRepository.pokemons().map(Pokemon::toUi) + } else { + pokemonListRepository.pokemons(query).map(Pokemon::toUi) + } + } catch (e: PokeException) { + handlePokemonError(e) + emptyList() + } finally { + _isLoading.value = false } - return pokemonListRepository.pokemons(query).map(Pokemon::toUi) } override fun navigateToPokemonDetail(pokemonId: Long) { diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/home/HomeActivity.kt b/android/app/src/main/java/poke/rogue/helper/presentation/home/HomeActivity.kt index 9b31a257..5ef059a6 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/home/HomeActivity.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/home/HomeActivity.kt @@ -13,7 +13,7 @@ import poke.rogue.helper.databinding.ActivityHomeBinding import poke.rogue.helper.presentation.ability.AbilityActivity import poke.rogue.helper.presentation.base.toolbar.ToolbarActivity import poke.rogue.helper.presentation.biome.BiomeActivity -import poke.rogue.helper.presentation.dex.PokemonActivity +import poke.rogue.helper.presentation.dex.PokemonListActivity import poke.rogue.helper.presentation.tip.TipActivity import poke.rogue.helper.presentation.type.TypeActivity import poke.rogue.helper.presentation.util.context.startActivity @@ -52,7 +52,7 @@ class HomeActivity : ToolbarActivity(R.layout.activity_home } is HomeNavigateEvent.ToDex -> - startActivity { + startActivity { logger.logClickEvent(NAVIGATE_TO_DEX) } diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/util/activity/ActivityExtension.kt b/android/app/src/main/java/poke/rogue/helper/presentation/util/activity/ActivityExtension.kt new file mode 100644 index 00000000..df929f35 --- /dev/null +++ b/android/app/src/main/java/poke/rogue/helper/presentation/util/activity/ActivityExtension.kt @@ -0,0 +1,22 @@ +package poke.rogue.helper.presentation.util.activity + +import android.app.Activity +import android.content.Context +import android.view.inputmethod.InputMethodManager + +fun Activity.hideKeyboard() { + val inputMethodManager = + getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + currentFocus?.let { view -> + inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) + view.clearFocus() + } +} + +fun Activity.show() { + val inputMethodManager = + getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + currentFocus?.let { view -> + inputMethodManager.showSoftInput(view, 0) + } +} diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/util/event/EventFlow.kt b/android/app/src/main/java/poke/rogue/helper/presentation/util/event/EventFlow.kt index 87434b57..0032caed 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/util/event/EventFlow.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/util/event/EventFlow.kt @@ -1,13 +1,9 @@ package poke.rogue.helper.presentation.util.event import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.util.concurrent.atomic.AtomicBoolean interface EventFlow : Flow { @@ -49,33 +45,3 @@ private class EventFlowSlot(val value: T) { fun markConsumed(): Boolean = consumed.getAndSet(true) } - -fun main() { - runBlocking { - val eventFlow = MutableSharedFlow(3) - eventFlow.emit("Hello") - eventFlow.emit("Hello2") - eventFlow.emit("Hello3") - launch { - var count = 2 - eventFlow.collect { - count-- - println(it) - if (count == 0) cancel() - } - } - launch { - var count = 2 - eventFlow.collect { - count-- - println(it) - if (count == 0) cancel() - } - } - delay(100) - launch { - println("Second collector") - eventFlow.collect { println(it) } - } - } -} diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/util/event/RefreshEventBus.kt b/android/app/src/main/java/poke/rogue/helper/presentation/util/event/RefreshEventBus.kt index ec289044..357b1dff 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/util/event/RefreshEventBus.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/util/event/RefreshEventBus.kt @@ -6,10 +6,10 @@ import kotlinx.coroutines.launch object RefreshEventBus { private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob()) - private val _event = MutableEventFlow(capacity = 1) + private val _event = MutableEventFlow() val event: EventFlow = _event.asEventFlow() - fun send() { + fun refresh() { coroutineScope.launch { _event.emit(Unit) } diff --git a/android/app/src/main/res/layout/activity_pokemon.xml b/android/app/src/main/res/layout/activity_pokemon.xml deleted file mode 100644 index ef61c706..00000000 --- a/android/app/src/main/res/layout/activity_pokemon.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - diff --git a/android/app/src/main/res/layout/fragment_pokemon_list.xml b/android/app/src/main/res/layout/activity_pokemon_list.xml similarity index 97% rename from android/app/src/main/res/layout/fragment_pokemon_list.xml rename to android/app/src/main/res/layout/activity_pokemon_list.xml index 5fc050fb..d1ce13d3 100644 --- a/android/app/src/main/res/layout/fragment_pokemon_list.xml +++ b/android/app/src/main/res/layout/activity_pokemon_list.xml @@ -13,7 +13,7 @@ + tools:context=".presentation.dex.PokemonListActivity"> () + // when + eventFlow.emit(1) + delay(10) + // then + eventFlow + .onEach { + println(">>> onEach: $it") + it shouldBe 1 + } + .launchIn(backgroundScope) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `EventFlow 는 element 를 공유하지 않는다`() = + runTest { + // given + val eventFlow = MutableEventFlow() + // when + eventFlow.emit(1) + delay(10) + // then + backgroundScope.launch { + launch { + eventFlow.collect { + println(">>> collect: $it") + it shouldBe 1 + } + } + launch { + eventFlow.collect { + println(">>> Never Collect AnyThing") + it shouldBe Int.MAX_VALUE + } + } + } + advanceTimeBy(100) + } +}