일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Flow
- Authentication
- 파이어베이스
- 회원가입
- UiState
- Kotlin
- DiffUtil
- 커스텀뷰
- 컴포즈
- XML
- 코틀린
- sharedFlow
- 플레이스토어
- Build variants
- 알고리즘
- Compose
- 로그인
- ListAdapter
- NavController
- coroutine
- NavHost
- 코딩테스트
- MVVM
- cleanarchitecture
- 뷰
- Android
- 리사이클러뷰
- 안드로이드
- Jetpack
- 클린아키텍처
- Today
- Total
Grusie 안드로이드 개발 기술 블로그
[Android] Compose + Paging 검색 기능 만들기(Flow - debounce) 본문
청년정책 앱을 만들던 도중, 검색 기능이 있어야겠다고 판단하여, 검색 기능을 만들게 되었다.
간단하게도 청년정책 앱의 Api는 검색 Api와, 전체를 불러오는 Api가 같은 url을 사용하는 것이라, 파라미터만 추가해주면 될 것 같았다.
참고
https://www.youthcenter.go.kr/opi/openApiPlcy.do
오픈 API | 청년정책 < 온통청년
www.youthcenter.go.kr
요청 파라미터
항목 | 타입 | 필수여부 | 설명 |
openApiVlak | String | Y | 마이페이지 > OpenAPI관리 에서 발급받은 인증키 |
display | Number | Y | 출력건수, 기본값 10, 최대 100까지 가능합니다. |
pageIndex | Number | Y | 조회할 페이지, 기본값 1 |
srchPolicyId | String | 정책 ID : 상세화면 조회 시 필수 | |
query | String | 정책명, 정책소개 정보검색(<polyBizSjnm>, <polyItcnCn>) | |
bizTycdSel | String | 정책유형 : 코드값을 사용합니다. 여러개 선택 시 ,(comma)로 분리하여 전송합니다.1. 일자리 분야(023010)2. 주거 분야(023020)3. 교육 분야(023030)4. 복지.문화 분야(023040)5. 참여.권리 분야(023050) | |
srchPolyBizSecd | String | 지역코드 - 시,도 : 코드값을 사용합니다.여러개 선택 시 ,(comma)로 분리하여 전송합니다.[코드 :시,도]003002001 :서울 | 003002002 :부산 | 003002003 :대구 | 003002004 :인천 | 003002005 :광주 | 003002006 :대전 | 003002007 :울산 | 003002008 :경기 | 003002009 :강원 | 003002010 :충북 | 003002011 :충남 | 003002012 :전북 | 003002013 :전남 | 003002014 :경북 | 003002015 :경남 | 003002016 :제주 | 003002017 :세종 | | |
keyword | String | 검색 키워드여러개 키워드 입력시 ,(comma)로 분리하여 전송합니다.예) 채용,취직,구직 |
Service
interface PolicyService {
@GET("youthPlcyList.do")
suspend fun getPolicyList(
@Query("openApiVlak")
apiKey: String = BuildConfig.POLICY_API_KEY, //api키
@Query("display") display: Int = 10, //보여줄 페이지 수
@Query("pageIndex") page: Int = 1, //조회할 페이지
@Query("query") query: String? = null, //정책명, 정책소개 정보검색
@Query("bizTycdSel") policyTypeCode: Int? = null, //정책 유형
@Query("srchPolyBizSecd") policyRegionCode: Int? = null, //지역코드
@Query("keyword") keyword: String? = null, //검색 키워드 예) 채용,취직,구직
@Query("srchPolicyId") policyId: String? = null //정책 Id
): PolicyList
}
필요한 정보들을 미리 Service에 담아두기 (추후 사용 예정)
우선 이번 시간에는 검색에 사용될 "query" 부분만 보도록 한다.
PagingSource
/**
* 검색을 위한 페이징 소스
**/
class PolicyPagingSource(
private val policyService: PolicyService,
private val query: String?,
private val policyTypeCode: Int?,
private val policyRegionCode: Int?,
private val keyword: String?
) : PagingSource<Int, PolicyItem>() {
override fun getRefreshKey(state: PagingState<Int, PolicyItem>): Int? {
return null
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PolicyItem> {
return try {
val page = params.key ?: 1
val policyList = policyService.getPolicyList(
page = page,
query = query,
policyTypeCode = policyTypeCode,
policyRegionCode = policyRegionCode,
keyword = keyword
)
val endOfPaginationReached = policyList.youthPolicy.isNullOrEmpty()
LoadResult.Page(
data = policyList.youthPolicy ?: emptyList(),
prevKey = if(page <= 1) null else page - 1,
nextKey = if(endOfPaginationReached) null else page + 1
)
} catch (exception: Exception){
LoadResult.Error(exception)
}
}
}
Paging3 라이브러리를 사용하고 있기에, PagingSource를 생성했다. policyService에 각 값들을 파라미터로 넘겨 데이터를 가져오도록 구현 해둠.
로컬DB에서의 검색은 혼동을 줄 수 있기에, 서버통신으로만 처리하기 위해 RemoteMediator가 아닌 PagingSource를 사용하였음.
RemoteSource
interface PolicyRemoteSource {
...
suspend fun getSearchPolicyList(
query: String?,
policyTypeCode: Int?,
policyRegionCode: Int?,
keyword: String?
): Flow<PagingData<PolicyItem>>
...
}
검색을 해서 받아온 데이터를 담을 RemoteSource에 함수를 추가한 뒤
RemoteSourceImpl
class PolicyRemoteSourceImpl @Inject constructor(
private val policyService: PolicyService,
private val policyInfoDB: PolicyInfoDB
) : PolicyRemoteSource {
private val policyInfoDao = policyInfoDB.policyDao()
...
override suspend fun getSearchPolicyList(
query: String?,
policyTypeCode: Int?,
policyRegionCode: Int?,
keyword: String?
): Flow<PagingData<PolicyItem>> {
val pagingSourceFactory = {
PolicyPagingSource(
policyService = policyService,
query = query,
policyTypeCode = policyTypeCode,
policyRegionCode = policyRegionCode,
keyword = keyword
)
}
return Pager(
config = PagingConfig(pageSize = 10, initialLoadSize = 10),
pagingSourceFactory = pagingSourceFactory
).flow
.flowOn(Dispatchers.IO)
}
...
}
RemoteSourceImpl에서 구현.
PagingSourceFactory에 PagingSource를 넣고, config의 정보만큼 데이터를 가져와서, flow형태로 방출한다.
Repository
interface PolicyRepository {
...
suspend fun getSearchPolicyList(
query: String? = null,
policyTypeCode: Int? = null,
policyRegionCode: Int? = null,
keyword: String? = null
): Flow<PagingData<PolicySimple>>
...
}
Repository에 검색 함수를 추가
RepositoryImpl
class PolicyRepositoryImpl @Inject constructor(
private val policyRemoteSource: PolicyRemoteSource,
private val policyLocalDataSource: PolicyLocalDataSource
) : PolicyRepository {
...
override suspend fun getSearchPolicyList(
query: String?,
policyTypeCode: Int?,
policyRegionCode: Int?,
keyword: String?
): Flow<PagingData<PolicySimple>> =
policyRemoteSource.getSearchPolicyList(
query = query,
policyTypeCode = policyTypeCode,
policyRegionCode = policyRegionCode,
keyword = keyword
).map { pagingData ->
pagingData.map {
it.toPolicySimple()
}
}
...
}
RepositoryImpl에서 구현.
RemoteSource로 부터 검색 결과를 받아와, 원하는 형태로 매핑을 해준 뒤 리턴해준다.
UseCase
data class PolicyUseCases(
val getPolicyListUseCase: GetPolicyListUseCase,
val getSearchPolicyListUseCase: GetSearchPolicyListUseCase, //추가
val getPolicyDetailUseCase: GetPolicyDetailUseCase
)
UseCase에도 추가해준다.
참고) UseCase를 제공해주는 Provides도 수정해주어야 함
UseCaseImpl
class GetSearchPolicyListUseCase(private val repository: PolicyRepository) {
suspend operator fun invoke(
query: String? = null,
policyTypeCode: Int? = null,
policyRegionCode: Int? = null,
keyword: String? = null
) = repository.getSearchPolicyList(
query = query,
policyTypeCode = policyTypeCode,
policyRegionCode = policyRegionCode,
keyword = keyword
)
}
UseCase구현체.
invoke()연산자를 통해 useCase를 구현.
로직은 repository를 호출하는 형태
ViewModel
private fun getPolicyList(query: String) {
viewModelScope.launch {
try {
setPolicyUiState(PolicySearchUiState.Loading)
policyUseCases.getSearchPolicyListUseCase(query = query)
.cachedIn(viewModelScope)
.collect { pagingData ->
_searchPolicyList.emit(pagingData)
}
} catch (e: Exception) {
setPolicyUiState(PolicySearchUiState.Error(e))
}
}
}
대망의 뷰모델.
usecase에서 getSearchPolicyListUseCase를 호출하여, 받아오는 Flow를 collect하여 pagingData를 Flow리스트로 emit해줌.
시작 시와 에러 발생 시, 각각 Loading, Error로 UiState 변경
Debounce를 활용한 지연처리
@OptIn(FlowPreview::class)
val debouncedSearchQuery: Flow<String?> = searchQuery
.debounce(Constant.SEARCH_TIME_DELAY)
.filter { it.trim().isNotEmpty() }
// Flow 가 되었기 때문에 distinctUntilChanged 를 달아줘 이전의 값에 대해 필터링
.distinctUntilChanged()
var currentQuery: String? = null
init {
viewModelScope.launch {
debouncedSearchQuery.collect { query ->
if (!query.isNullOrEmpty()) {
currentQuery = query
doSearch()
}
}
}
}
검색 버튼으로도 처리하겠지만, 입력값이 없다면 자동으로 검색을 요청하기 위해 Flow의 debounce를 활용하였음.
이전 값과 같은 값은 처리하지 않도록 하기 위해 distinctUntilChanged() 사용
init에서 collect하여, 비어있지 않으면, 검색을 하는 로직
SearchScreen
OutlinedTextField(
modifier = Modifier
.fillMaxWidth(),
value = searchQuery,
singleLine = true,
trailingIcon = {
Icon(imageVector = Icons.Filled.Search, contentDescription = "search_icon")
},
onValueChange = { changeSearchQuery(it) },
keyboardActions = KeyboardActions(onDone = {
onSearch()
})
)
TextField에서 값의 변경을 처리하고, 변경된 값은 viewModel의 _searchQuery를 변경하여, SEARCH_TIME_DELAY초가 지나면 검색을 처리한다.
val searchPolicyList = viewModel.searchPolicyList.collectAsLazyPagingItems()
PolicySimpleList(
policyList = searchPolicyList,
navController = navController,
loading = { loading -> viewModel.setPolicyUiState(if (loading) PolicySearchUiState.Loading else PolicySearchUiState.Success) }
)
검색 결과를 받아서, LazyColumn에 뿌려준다.
원하는대로 잘 나오는 것을 볼 수 있다.
코드
GitHub - Grusie/PolicyInfoApp: Clean Architecture + MVVM + Hilt + Compose를 적용한 청년정책 앱
GitHub - Grusie/PolicyInfoApp: Clean Architecture + MVVM + Hilt + Compose를 적용한 청년정책 앱
Clean Architecture + MVVM + Hilt + Compose를 적용한 청년정책 앱 - Grusie/PolicyInfoApp
github.com
후기
useCase를 추가하고 Repository를 추가하고 서버통신의 파라미터를 늘려가면서, 클린 아키텍처에 대한 이해를 더 하였고, 서버통신에 관한 것을 뷰에서 완전 모른다는 것에 더더욱 매력을 느꼈다. 코드 수정하는 것을 간편하게 했던 것 같아 좋았던 것 같다.
하지만 컴포즈를 사용하면서 UiState가 변경 될 때 화면이 다시 그려지면서 이전 화면이 깜빡이던 현상을 고치느라 3일 가까이 쓴 것 같다. 사이드 이펙트를 사용하여 버그를 잡을 수 있었고, 컴포즈에 대해서 다음 번에 한 번 더 정리해야겠다고 생각하였다.
'안드로이드 개발 > 라이브러리' 카테고리의 다른 글
[Android]RxJava 사용하기 (0) | 2024.04.15 |
---|---|
[Android] Retrofit2 사용법 (0) | 2024.03.27 |
[Android] JetPack:Compose 네비게이션(navigation) - 2 (0) | 2024.03.07 |
[Android] JetPack:Compose 네비게이션(navigation) - 1 (0) | 2024.03.06 |
[Android] Paging 3.0 라이브러리 (0) | 2024.02.29 |