안녕하세요. 오늘은 안드로이드에서 kotlin을 이용해 안드로이드에서 검색기능 및 검색어 자동완성 기능 구현하는 방법에 대해 알아보겠습니다.
검색어 자동완성 기능을 구현하기 위해서는 우선적으로 Debounce의 개념을 이해해야 합니다.
1. Debounce란
Debounce는 마지막 이벤트가 발생한 후 일정 시간 동안 추가적인 이벤트가 발생하지 않을 때, 마지막 이벤트를 처리합니다. 다시 말해, 연속적으로 발생하는 이벤트 중 마지막 이벤트만을 실행합니다. 이를 통해 너무 자주 발생하는 이벤트를 하나로 묶어 처리할 수 있습니다.
그 예시로 사용자가 검색 입력창에 빠르게 여러 문자를 입력할 때, 입력이 끝난 후 일정 시간 동안 추가 입력이 없으면 마지막 입력에 대해 검색 요청을 보냅니다.
이러한 Debounce의 특징을 이용하여 사용자가 검색창에 text를 입력하는 상황에서,
입력이 끝난 후 특정 시간종안 추가 입력이 없으면 그 시점에 이벤트(검색어 자동 완성)가 발생합니다.
만약, 사용자가 여러번 입력하면, 입력이 끝날 때 까지 이벤트가 발생하지 않다가 마지막에 한 번만 발생합니다.
이러한 원리를 이용하여 검색어 자동완성 기능을 구현 할 수 있게 됩니다.
2. 검색기능 및 검색어 자동완성기능 구현
검색기능 및 검색어 자동완성 기능을 본격적으로 구현해보겠습니다.
우선 UI 코드입니다.
@Composable
fun TabFriend2(
friendViewModel: FriendViewModel,
navController: NavHostController,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
var searchQuery2 by remember { mutableStateOf("") }
val searchResults2 by friendViewModel.searchResults2.collectAsState()
val users by friendViewModel.users.collectAsState()
var showDetailUser by remember { mutableStateOf(false) }
var selectedUserEmail by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
friendViewModel.readFriendRequest(context)
}
//friendViewModel.readFriendRequest(context)
if(showDetailUser && selectedUserEmail != null){
UserInfoScreen(
navController = navController,
userEmail = selectedUserEmail,
friendViewModel = friendViewModel
)
} else {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(text = "유저 검색")
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
value = searchQuery2,
singleLine = true,
onValueChange = {
searchQuery2 = it
friendViewModel.onSearchQueryChange2(searchQuery2)
},
label = { Text("Search User") }
)
val itemsToShow = if (searchQuery2.isNotEmpty()) searchResults2 else users
fun onUserClick(userEmail: String){
navController.navigate("friend/info/$userEmail")
Log.d("home screen", "search test route = ${userEmail}")
}
//search user result
LazyColumn(contentPadding = PaddingValues(16.dp, 8.dp)) {
items(
items = searchResults2,
//items = itemsToShow,
itemContent = { user ->
UserListItem(user = user, navController = navController) {
onUserClick(user.userEmail)
}
}
)
}
Text(text = "친구 요청 목록")
Divider(color = Color.Black, thickness = 1.dp)
RecyclerViewFriendRequestContent(viewModel = friendViewModel, navController = navController, context = context){ userEmail ->
selectedUserEmail = userEmail
showDetailUser = true
}
}
}
}
위 코드에서 OutlinedTextField에 입력된 값은 by remember mutableStateOf 형식으로 선언된 searchQuery2가 value 형식으로저장됩니다.
이후에는 viewModel의 onSearchQueryChange2 method에 searchQuery2를 parameter로 전달하게 됩니다.
private val searchQuery2 = MutableStateFlow("")
fun onSearchQueryChange2(keyword: String){
searchQuery2.value = keyword
}
이에 대하여 viewModel의 onSearchQueryChange2 method에서는 해당 parameter를 viewModel에 정의한 searchQuery2변수에 저장해줍니다.
init {
viewModelScope.launch {
searchQuery2
.debounce(300)
.collectLatest { query ->
if(query.isNotEmpty()) {
searchUser(query)
} else {
_searchResults2.value = emptyList()
}
}
}
이후에는 viewModel의 init에서 searchQuery2에 대하여 debounce를 이용하여 마지막 입력 후 300ms 까지 추가적인 입력이 없다면, viewModel에 정의된 searchUser method를 이용해 입력받은 값을 parameter로 Spring에 해당 검색어와 일치하는 결과를 요청하게 됩니다.
fun searchUser(keyword: String){
viewModelScope.launch(Dispatchers.IO) {
val userEmail = userRepository.getMostRecentUserName()
val response = SecretDiaryObject.getRetrofitSDService.searchUser(keyword, userEmail!!)
if(response.isSuccessful){
response.body()?.let {
_searchResults2.value = it
}
}else{
val errorBody = response.errorBody()?.string()
val statusCode = response.code()
Log.d("Search_read", "Failed: StatusCode = $statusCode, Error = $errorBody")
}
}
}
searchUser의 경우 room을 이용하여 현재 로그인한 유저의 이메일을 보내,
검색 결과에 본인은 포함되지 않게 하기위한 parameter로 사용하게 됩니다.
검색 결과에 대해서는 _searchResult2.value를 이용해 해당 변수에 결과를 저장하게 됩니다.
@GET("search/{keyword}/{userEmail}")
suspend fun searchUser(@Path("keyword") keyword: String, @Path("userEmail") userEmail: String): Response<List<RUserModel>>
이제는 viewModel에 정의한 searchUser method가 호출하는 retrofit2 API method입니다.
해당 method의 경우 @Path를 이용하여 Spring server에게 검색어와 본인 이메일을 건내주고 있습니다.
이제부터는 android에서 보낸 요청을 Spring에서 받아올 차례입니다.
@GetMapping("search/{keyword}/{userEmail}")
public List<RUserRequestDTO> searchUser2(@PathVariable("keyword") String keyword, @PathVariable("userEmail") String userEmail){
return userService.getSearchUser2(keyword, userEmail);
}
spring의 controller에서는 위와같은 방법으로 해당 요청을 받아옵니다.
이후에는
@Override
public List<RUserRequestDTO> getSearchUser2(String keyword, String userEmail){
List<User> entities = userRepository.findByEmailContainingAndEmailNot(keyword, userEmail);
List<RUserRequestDTO> dtos = new ArrayList<>();
for(User entity : entities){
RUserRequestDTO userRequestDTO = new RUserRequestDTO();
userRequestDTO.setUserId(entity.getId());
userRequestDTO.setUserEmail(entity.getEmail());
userRequestDTO.setUserNickName(entity.getName());
userRequestDTO.setUserText(entity.getText());
userRequestDTO.setUserImgPath(entity.getUserImg());
dtos.add(userRequestDTO);
}
return dtos;
}
repository에 JPA 문법을 이용하여 userEmail에 해당하는 유저를 제외하고, keyword를 포함하고 있는 유저만 가져오는 method를 repository에 정의하여 구현하면 됩니다.
이렇게 안드로이드에서 검색기능 및 검색어 자동완성 기능 구현하는 방법은 모두 설명하였습니다.
하지만 Debounce라는 개념과 같이 소개되는 Throttle에 대해서도 이번 게시물에서 알아보겠습니다.
2. Throttle이란
Throttle은 특정 시간 간격 내에서 한 번만 이벤트를 처리하도록 제한합니다. 일정한 시간 간격을 두고 이벤트가 발생하게 하여, 일정 주기로 이벤트가 실행됩니다. 즉, 이벤트가 너무 자주 발생하는 것을 방지하고, 일정 시간 간격으로만 실행됩니다.
그 예시로 사용자가 페이지를 스크롤할 때, 스크롤 이벤트가 너무 자주 발생하는 것을 방지하고, 일정 간격으로 스크롤 이벤트를 처리하여 부하를 줄입니다.
아래는 Kotlin Coroutine을 이용한 Throttle 구현 방식예제입니다.
import kotlinx.coroutines.flow.flow
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
var lastEmissionTime = 0L
collect { value ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmissionTime >= windowDuration) {
emit(value)
lastEmissionTime = currentTime
}
}
}
이 throttleFirst 함수는 Kotlin의 Flow에 대해 확장함수로 정의된 것으로, 일정한 시간 동안 발생한 이벤트 중 첫 번째 이벤트만 처리하고, 그 이후의 이벤트는 windowDuration에 설정한 시간까지무시하는 동작을 구현합니다.
이후에는, 다시 이벤트를 발생시키고 windowDuration에 설정한 시간까지 무시하는 동작을 반복하는 함수가 됩니다.
이렇게 kotlin을 이용해 안드로이드에서 검색기능 및 검색어 자동완성 기능 구현하는 방법에 대해서는 마무리하도록 하겠습니다.
다음번에는 security 관련 주제로 돌아오겠습니다.
'안드로이드 앱개발' 카테고리의 다른 글
Android에서 Spring Security를 이용한 자동 로그인 구현 (1) | 2024.10.19 |
---|---|
안드로이드 에뮬레이터에서 이미지 불러와서 Spring 모바일 앱 서버를 이용해 DB에 저장하기 (6) | 2024.10.08 |
Retrofit2를 이용한 안드로이드와 스프링 서버 통신(안드로이드편)(안드로이드 서버통신) (3) | 2024.10.03 |
Kotlin Coruoutins과 비동기에 관하여 (1) | 2024.10.02 |
Android Room 사용하기 (4) | 2024.09.28 |