Skip to content

Commit

Permalink
[1.1.0/AN_FEAT] 바이옴 리스트 검색 기능 구현 (#320)
Browse files Browse the repository at this point in the history
* feat: toolbar에 검색 아이콘 넣기

* feat: biome repository 검색 구현

* feat: biome 리스트 검색 바인딩 어뎁터

* test: 바이옴 조회 repository 테스트 작성

* feat: 바이옴 검색 기능 구현

* feat: 검색시 리싸이클러뷰 스크롤 항상 맨위로 이동

* feat: 키보드 아웃포커싱 내리기

* style: ktFormat

* refactor: 리싸이클러뷰 스크롤 위치 이동

* style: ktFormat
  • Loading branch information
kkosang authored Sep 24, 2024
1 parent 0bd8290 commit 4c0fd35
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import poke.rogue.helper.presentation.base.error.ErrorHandleViewModel
import poke.rogue.helper.presentation.biome.detail.BiomeDetailActivity
import poke.rogue.helper.presentation.biome.guide.BiomeGuideActivity
import poke.rogue.helper.presentation.biome.model.toUi
import poke.rogue.helper.presentation.util.activity.hideKeyboard
import poke.rogue.helper.presentation.util.context.startActivity
import poke.rogue.helper.presentation.util.logClickEvent
import poke.rogue.helper.presentation.util.repeatOnStarted
Expand All @@ -36,6 +37,7 @@ class BiomeActivity : ErrorHandleActivity<ActivityBiomeBinding>(R.layout.activit
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initListener()
initView()
initAdapter()
initObservers()
Expand All @@ -46,6 +48,13 @@ class BiomeActivity : ErrorHandleActivity<ActivityBiomeBinding>(R.layout.activit
binding.lifecycleOwner = this
}

private fun initListener() {
binding.rvBiomeList.setOnTouchListener { _, _ ->
hideKeyboard()
false
}
}

private fun initAdapter() {
binding.rvBiomeList.apply {
adapter = biomeAdapter
Expand Down Expand Up @@ -78,14 +87,16 @@ class BiomeActivity : ErrorHandleActivity<ActivityBiomeBinding>(R.layout.activit
}

repeatOnStarted {
viewModel.biome.collect { biome ->
viewModel.biomes.collect { biome ->
when (biome) {
is BiomeUiState.Loading -> {
binding.biomeLoading.isVisible = true
}

is BiomeUiState.Success -> {
biomeAdapter.submitList(biome.data.toUi())
biomeAdapter.submitList(biome.data.toUi()) {
binding.rvBiomeList.scrollToPosition(0)
}
binding.biomeLoading.isVisible = false
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package poke.rogue.helper.presentation.biome

interface BiomeQueryHandler {
fun queryName(name: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package poke.rogue.helper.presentation.ability

import androidx.appcompat.widget.SearchView
import androidx.databinding.BindingAdapter
import poke.rogue.helper.presentation.biome.BiomeQueryHandler

@BindingAdapter("onQueryTextChange")
fun setOnQueryTextListener(
searchView: SearchView,
onQueryTextChangeListener: BiomeQueryHandler,
) {
searchView.setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = false

override fun onQueryTextChange(newText: String?): Boolean {
onQueryTextChangeListener.queryName(newText.toString())
return true
}
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ package poke.rogue.helper.presentation.biome

import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.mapLatest
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.model.Biome
Expand All @@ -23,29 +27,33 @@ class BiomeViewModel(
logger: AnalyticsLogger = analyticsLogger(),
) :
ErrorHandleViewModel(logger),
BiomeUiEventHandler {
private val _biome = MutableStateFlow<BiomeUiState<List<Biome>>>(BiomeUiState.Loading)
val biome = _biome.asStateFlow()

BiomeUiEventHandler,
BiomeQueryHandler {
private val _navigationToDetailEvent = MutableSharedFlow<String>()
val navigationToDetailEvent: SharedFlow<String> = _navigationToDetailEvent.asSharedFlow()

private val _navigateToGuideEvent = MutableSharedFlow<Unit>()
val navigateToGuideEvent: SharedFlow<Unit> = _navigateToGuideEvent.asSharedFlow()

init {
refreshEvent
.onStart {
emit(Unit)
}.onEach {
updateBiomes()
}.launchIn(viewModelScope)
}
val searchQuery = MutableStateFlow("")

private fun updateBiomes() {
viewModelScope.launch(errorHandler) {
val biomes = biomeRepository.biomes()
_biome.value = BiomeUiState.Success(biomes)
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
val biomes: StateFlow<BiomeUiState<List<Biome>>> =
searchQuery
.debounce(300L)
.mapLatest { query ->
val biomes = biomeRepository.biomes(query)
BiomeUiState.Success(biomes)
}
.stateIn(
viewModelScope + errorHandler,
SharingStarted.WhileSubscribed(5000L),
BiomeUiState.Loading,
)

override fun queryName(name: String) {
viewModelScope.launch {
searchQuery.emit(name)
}
}

Expand Down
9 changes: 9 additions & 0 deletions android/app/src/main/res/layout/activity_biome.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
android:padding="12dp"
android:src="@drawable/ic_biome_map" />

<androidx.appcompat.widget.SearchView
app:onQueryTextChange="@{vm}"
style="@style/CustomSearchViewStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:theme="@style/CustomSearchViewTheme"
app:queryHint="@string/biome_search_hint" />

</com.google.android.material.appbar.MaterialToolbar>

<androidx.recyclerview.widget.RecyclerView
Expand Down
1 change: 1 addition & 0 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
<!-- biome -->
<string name="biome_title_name">바이옴 도감</string>
<string name="biome_guide_url">https://wiki.pokerogue.net/ko:biomes:biomes</string>
<string name="biome_search_hint">바이옴 이름으로 검색하세요.</string>
<string-array name="biome_tab_titles">
<item>체육관</item>
<item>보스</item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class BiomeViewModelTest {
runTest {
// given,when
viewModel = BiomeViewModel(repository)
val biomes = viewModel.biome.first { it is BiomeUiState.Success }
val biomes = viewModel.biomes.first { it is BiomeUiState.Success }
val actualBiomes = (biomes as BiomeUiState.Success).data

// then
Expand Down Expand Up @@ -68,4 +68,26 @@ class BiomeViewModelTest {
// then
actual shouldBe Unit
}

@Test
fun `올바른 바이옴 이름을 검색했을 때, 해당하는 바이옴을 반환한다`() =
runTest {
// when
val biome = repository.biomes("악지")

// then
val actual = biome.find { it.name == "악지" }?.name
val expect = "악지"
actual shouldBe expect
}

@Test
fun `잘못된 바이옴 이름을 검색했을 때, 빈 리스트를 반환한다`() =
runTest {
// when
val biome = repository.biomes("잘못된 이름")

// then
biome.size shouldBe 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ import poke.rogue.helper.data.model.BiomeDetail
interface BiomeRepository {
suspend fun biomes(): List<Biome>

suspend fun biomes(query: String): List<Biome>

suspend fun biomeDetail(id: String): BiomeDetail
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import poke.rogue.helper.data.datasource.RemoteBiomeDataSource
import poke.rogue.helper.data.model.Biome
import poke.rogue.helper.data.model.BiomeDetail
import poke.rogue.helper.data.utils.logBiomeDetail
import poke.rogue.helper.stringmatcher.has

class DefaultBiomeRepository(
private val remoteBiomeDataSource: RemoteBiomeDataSource,
Expand All @@ -20,6 +21,13 @@ class DefaultBiomeRepository(
return cachedBiomes
}

override suspend fun biomes(query: String): List<Biome> {
if (query.isBlank()) {
return biomes()
}
return biomes().filter { it.name.has(query) }
}

override suspend fun biomeDetail(id: String): BiomeDetail {
return remoteBiomeDataSource.biomeDetail(id).also {
analyticsLogger.logBiomeDetail(id, it.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ import poke.rogue.helper.data.model.biome.BossPokemon
import poke.rogue.helper.data.model.biome.GymPokemon
import poke.rogue.helper.data.model.biome.WildPokemon
import poke.rogue.helper.data.repository.BiomeRepository
import poke.rogue.helper.stringmatcher.has

class FakeBiomeRepository : BiomeRepository {
override suspend fun biomes(): List<Biome> = BIOMES

override suspend fun biomes(query: String): List<Biome> {
return BIOMES.filter { biome -> biome.name.has(query) }
}

override suspend fun biomeDetail(id: String): BiomeDetail = BIOME_DETAIL[id] ?: throw IllegalArgumentException("Invalid biome ID")

companion object {
Expand Down

0 comments on commit 4c0fd35

Please sign in to comment.