내가 만든 앱에서 순간검색을 지원하고 싶었던 순간이 많았는데...
원래 되는대로 검색 요청 날리다가 이번에 새로운 방법을 알게되서 적어본다.
Kotlin에서 Flow가 뭘까...공부하다가 나온 예제에서 발견했다.
완성하면 이렇게된다.
전체 프로젝트 Github
https://github.com/lx5475/Kotlin-Flow-Instant-Search
간단하게 치킨집 목록을 검색하는 걸로 시작해보도록하겠다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | dependencies { // RecyclerView implementation "androidx.recyclerview:recyclerview:1.1.0" // ViewModel def lifecycle_version = "2.2.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Koin def koin_version = '2.1.6' implementation "org.koin:koin-core:$koin_version" implementation "org.koin:koin-android-viewmodel:$koin_version" } | cs |
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_result" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/et_search"/> <EditText android:id="@+id/et_search" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> | cs |
MainActivity.kt
et_search.onTextChanged는 내가 만든 Extension인데 TextWatcher > onTextChanged 내부에서 구현해주면된다.
EditText에서 문자가 업데이트되는대로 queryChannel에 보내주고있다.
이렇게 해서 검색어를 view에서 ViewModel단으로 옮길 수 있다.
사용할 수 있는 메소드는 offer()와 send() 두가지가 있다.
* 문서에보면 offer는 즉시 element를 채널에 동기적으로 전송한다고 되어있고
send()는 element를 채널로 전송하되, 채널의 버퍼가 가득차거나 존재하지 않는 경우 지연시킨다라고 되어있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by viewModel() private lateinit var adapter: ChickenAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) init() // 검색 결과를 observe하여 Adapter에 등록 viewModel.searchResult.observe(this, Observer { adapter.submitList(it) }) et_search.setText("") // 처음에 모든 치킨집 리스트 보여주기 위함 } private fun init() { adapter = ChickenAdapter() // RecyclerView 설정 rv_result.adapter = adapter rv_result.setHasFixedSize(true) rv_result.layoutManager = LinearLayoutManager(this) // EditText 입력 값에 변화가 있으면 BroadcastChannel로 값 전송 et_search.onTextChanged { s, start, before, count -> val queryText = s.toString() // Channel에 queryText 전송, Channel 용량을 침범하지 않았다면 true 아니면 false 리턴 viewModel.queryChannel.offer(queryText) } } } | cs |
1 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" | cs |
1 2 3 4 5 6 7 | et_search.onTextChanged { s, start, before, count -> lifecycleScope.launch { val queryText = s.toString() // Channel에 queryText 전송, Channel 용량을 침범하지 않았다면 true 아니면 false 리턴 viewModel.queryChannel.send(queryText) } } | cs |
MainRepository.kt
Koin에 대한 포스팅은 아니니까 생략하고~
Api를 호출하는 걸 흉내내기 위해서 Repository에 다음과 같이 구현해주었다.
검색어가 들어오면 해당 텍스트가 들어가는 아이템만 뽑아서 반환한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | class MainRepository() { private val list = arrayListOf( "범프리카 인생치킨", "더꼬치다", "치킨대가", "처갓집", "오 순살", "후라이드참잘하는집", "박옥녀", "치킨더홈", "꾸브라꼬숯불두마리치킨", "갓튀긴후라이드", "쌀통닭", "21세기굽는치킨", "림스치킨", "치킨신드롬", "철인7호", "동키치킨", "굽네치킨", "땅땅치킨", "썬더치킨", "BBQ", "Bhc", "장수통닭", "동근이숯불두마리치킨", "갓튀긴후라이드" ) suspend fun search(text: String?): List<String> { delay(100) // 실제 Api 호출인것처럼 하기위해 딜레이 넣음 return list.filter { it.contains(text ?: "") } } } | cs |
MainViewModel.kt
mapLatest에서 Api 호출 결과를 받는다.
만약에 이전에 검색어를 이용해서 Api 결과를 받아오는 도중 새로운 검색어가 들어온다면,
이전 호출은 취소된다.
catch는 위에 블록까지만 잡아주므로 작성 순서에 유의하자!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | @ExperimentalCoroutinesApi @FlowPreview class MainViewModel( application: Application, repository: MainRepository ) : AndroidViewModel(application) { private val SEARCH_TIMEOUT = 300L /* Channel.CONFLATED는 ConflatedChannel를 생성 ConflatedChannel: 보내진 element 중에서 하나의 element만 버퍼링하므로서 Receiver가 항상 최근에 보내진 element를 가져올 수 있도록함 */ val queryChannel = BroadcastChannel<String>(Channel.CONFLATED) val searchResult = queryChannel .asFlow() // BroadcastChannel을 hot flow로 바꿈 // search() 호출 속도를 조절할 수 있음. // 해당 ms동안 새로운 텍스트를 입력하지 않으면 search() 호출 .debounce(SEARCH_TIMEOUT) .mapLatest { text -> // 여기에 실제 Api를 호출하는 코드를 적어주시면 됩니다. withContext(Dispatchers.IO) { repository.search(text) } } .catch { e: Throwable -> // 에러 핸들링은 여기서! e.printStackTrace() } .asLiveData() } | cs |
더 궁금한 것이 있다면 아래 링크를 참조
역시나 포스팅은 귀찮지만 내가 얼마나 모르고 사용했는지 알게되네...
Broadcast Channel Documentation
Instant Search with Kotlin Coroutines
https://www.hellsoft.se/instant-search-with-kotlin-coroutines/
Kotlin Flow for Android: Getting Started
https://www.raywenderlich.com/9799571-kotlin-flow-for-android-getting-started
'프로그래밍 > Android' 카테고리의 다른 글
[안드로이드] MVVM 아키텍쳐 예제 Pokedex (0) | 2020.09.28 |
---|---|
[안드로이드] Dexter로 권한요청 쉽게하기 (Permission request) (0) | 2020.09.28 |
Domain / Data / Presentation의 이해 (0) | 2020.07.02 |
64bit 안드로이드에서 32bit 라이브러리를 불러오지 못 할 때 (0) | 2020.05.15 |
[안드로이드] URL을 이용해 앱의 특정 페이지 열기 (0) | 2020.04.08 |