Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

부산대 Android_이창욱_2주차_과제 #33

Open
wants to merge 13 commits into
base: ichanguk
Choose a base branch
from
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,27 @@
# android-map-keyword

- 카카오 테크 캠퍼스 과제(검색어 저장) 수행을 위한 저장소입니다.

## feature

### 1단계 - 로컬 데이터

1. 검색어 입력 및 검색 결과를 표시할 기본 레이아웃 구현

2. 검색에 사용될 데이터를 로컬 베이스에 생성
- SQLite를 사용

### 2단계 - 검색

1. 검색어를 입력하면 검색 결과 목록이 표시된다.
- 리사이클러뷰 사용(세로 스크롤)

2. 입력한 검색어는 X를 눌러서 삭제할 수 있다.

3. 검색 결과 목록에서 하나의 항목을 선택할 수 있다.
- 선택된 항목은 검색어 저장 목록에 추가된다.
- 리사이클러뷰 사용(가로 스크롤)

4. 저장된 검색어는 X를 눌러서 삭제할 수 있다.

5. 이미 검색어 저장 목록에 있는 검색어를 검색 결과 목록에서 선택한 경우 기존 검색어는 삭제하고 다시 추가한다.
8 changes: 8 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("org.jlleitschuh.gradle.ktlint")
}

android {
Expand Down Expand Up @@ -36,6 +38,11 @@ android {

buildFeatures {
viewBinding = true
dataBinding = true
}

dataBinding {
enable = true
}
}

Expand All @@ -47,6 +54,7 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
Expand Down
270 changes: 269 additions & 1 deletion app/src/main/java/campus/tech/kakao/map/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,279 @@
package campus.tech.kakao.map

import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import campus.tech.kakao.map.databinding.ActivityMainBinding
import campus.tech.kakao.map.databinding.ItemPlaceBinding
import campus.tech.kakao.map.databinding.ItemSavedSearchWordBinding
import campus.tech.kakao.map.model.Place
import campus.tech.kakao.map.model.SavedSearchWord
import campus.tech.kakao.map.viewmodel.PlaceViewModel
import campus.tech.kakao.map.viewmodel.SavedSearchWordViewModel

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val placeViewModel by lazy { ViewModelProvider(this)[PlaceViewModel::class.java] }
private val savedSearchWordViewModel by lazy { ViewModelProvider(this)[SavedSearchWordViewModel::class.java] }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by viewModels()

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멘토링 때에도 말씀드렸다시피 전략적인 선택을 하면 됩니다. 다만 지금 현재 화면의 요구사항이 굉장히 단순하기 때문에 저는 fragment 로 따로 분리하지는 않을 것 같아요. 확장요구에 대한 낌새가 보일 때 fragment 로 분리하는 작업을 진행할 것 같습니다.

또한 같은 Fragment 혹은 Activity 내에서 ViewModel 을 여러개 사용하는 것은 관점에 따라 좋을 수도 나쁠 수도 있습니다. 위에도 언급했다시피 이 화면 자체의 요구사항이 매우 간단하기 떄문에 ViewModel 을 나누기 보다 저는 하나의 ViewModel 로 진행할 것 같습니다 :)


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupBinding()
setupDummyData()
setupViews()
observeViewModels()
}

private fun setupBinding() {
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.apply {
this.placeViewModel = [email protected]
this.savedSearchWordViewModel = [email protected]
this.lifecycleOwner = this@MainActivity
}
}

/**
* RecyclerView들을 설정하는 함수.
*/
private fun setupRecyclerViews() {
setSearchResultRecyclerView()
setSavedSearchWordRecyclerView()
}

/**
* 테스트용 더미 데이터를 place db에 삽입하기 위한 함수.
*/
private fun setupDummyData() {
placeViewModel.clearAllPlaces()
testDataInsert()
}

/**
* 테스트용 더미 데이터(카페, 약국)를 삽입하는 함수
*/
private fun testDataInsert() {
insertPlaces("서울 성동구 성수동", "카페")
insertPlaces("서울 강남구 대치동", "약국")
insertPlaces("서울 강남구 수서동", "약국")
}

/**
* place 데이터를 db에 삽입하는 함수.
*
* @param address 저장할 주소값
* @param category 저장할 카테고리값
*/
private fun insertPlaces(
address: String,
category: String,
) {
repeat(20) { idx ->
placeViewModel.insertPlace(
Place(
name = "$category ${idx + 1}",
address = "$address ${idx + 1}",
category = category,
),
)
}
}

/**
* view들에 필요한 작업을 처리하는 함수.
*/
private fun setupViews() {
setClearImageViewClickListener()
setSearchEditText()
setupRecyclerViews()
}

/**
* 검색 EditText가 변경되면 placeViewModel을 통해 검색을 수행하도록 하는 함수.
*/
private fun setSearchEditText() {
binding.searchEditText.addTextChangedListener { editable ->
val searchText = editable.toString().trim()
placeViewModel.searchPlacesByCategory(searchText)
}
}

/**
* clearImageView의 클릭 리스너를 설정하는 함수.
*
* searchEditText의 text를 null로 변경.
*/
private fun setClearImageViewClickListener() {
binding.searchClearImageView.setOnClickListener {
binding.searchEditText.text = null
}
}

interface OnPlaceItemClickListener {
fun onPlaceItemClicked(place: Place)
}

/**
* 검색 결과를 표시하는 RecyclerView를 설정하는 함수.
*
* - `placeItemClickListener` : placeItem을 누르면 검색어가 저장되도록 하는 클릭 리스너 interface 구현 객체
*/
private fun setSearchResultRecyclerView() {
val placeItemClickListener =
object : OnPlaceItemClickListener {
override fun onPlaceItemClicked(place: Place) {
savedSearchWordViewModel.insertSearchWord(
SavedSearchWord(
name = place.name,
placeId = place.id,
),
)
}
}
binding.searchResultRecyclerView.adapter = ResultRecyclerViewAdapter(placeItemClickListener)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory Leak, Garbage Collection

binding.searchResultRecyclerView.layoutManager = LinearLayoutManager(this)
}

class ResultRecyclerViewAdapter(private val clickListener: OnPlaceItemClickListener) :
ListAdapter<Place, ResultRecyclerViewAdapter.PlaceViewHolder>(PlaceDiffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): PlaceViewHolder {
val binding = ItemPlaceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PlaceViewHolder(binding)
}

override fun onBindViewHolder(
holder: PlaceViewHolder,
position: Int,
) {
val place = getItem(position)

holder.binding.place = place
holder.itemView.setOnClickListener {
clickListener.onPlaceItemClicked(place)
}
}

class PlaceViewHolder(val binding: ItemPlaceBinding) : RecyclerView.ViewHolder(binding.root)

private class PlaceDiffCallback : DiffUtil.ItemCallback<Place>() {
override fun areItemsTheSame(
oldItem: Place,
newItem: Place,
): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(
oldItem: Place,
newItem: Place,
): Boolean {
return oldItem == newItem
}
}
}

interface OnSavedSearchWordClearImageViewClickListener {
fun onSavedSearchWordClearImageViewClicked(savedSearchWord: SavedSearchWord)
}

/**
* SavedSearchWordRecyclerView를 설정하는 함수.
*
* - `savedSearchWordClearImageViewClickListener` : clear 버튼을 누르면 해당 저장된 검색어가 사라지도록 하는 클릭리스너 interface 구현 객체
*/
private fun setSavedSearchWordRecyclerView() {
val savedSearchWordClearImageViewClickListener =
object : OnSavedSearchWordClearImageViewClickListener {
override fun onSavedSearchWordClearImageViewClicked(savedSearchWord: SavedSearchWord) {
savedSearchWordViewModel.deleteSearchWordById(savedSearchWord)
}
}
binding.savedSearchWordRecyclerView.adapter =
SavedSearchWordRecyclerViewAdapter(savedSearchWordClearImageViewClickListener)
binding.savedSearchWordRecyclerView.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
}

class SavedSearchWordRecyclerViewAdapter(private val clickListener: OnSavedSearchWordClearImageViewClickListener) :
ListAdapter<SavedSearchWord, SavedSearchWordRecyclerViewAdapter.SavedSearchWordViewHolder>(SavedSearchWordDiffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): SavedSearchWordViewHolder {
val binding = ItemSavedSearchWordBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SavedSearchWordViewHolder(binding)
}

override fun onBindViewHolder(
holder: SavedSearchWordViewHolder,
position: Int,
) {
val savedSearchWord = getItem(position)
holder.binding.savedSearchWord = savedSearchWord
holder.itemView.setOnClickListener {
clickListener.onSavedSearchWordClearImageViewClicked(savedSearchWord)
}
}

class SavedSearchWordViewHolder(
val binding: ItemSavedSearchWordBinding,
) : RecyclerView.ViewHolder(binding.root)

private class SavedSearchWordDiffCallback : DiffUtil.ItemCallback<SavedSearchWord>() {
override fun areItemsTheSame(
oldItem: SavedSearchWord,
newItem: SavedSearchWord,
): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(
oldItem: SavedSearchWord,
newItem: SavedSearchWord,
): Boolean {
return oldItem == newItem
}
}
}

/**
* viewModel을 관찰하도록 하는 함수.
*/
private fun observeViewModels() {
observeSearchResults()
observeSavedSearchWords()
}

/**
* 검색 결과를 관찰하고, RecyclerView에 결과를 반영하는 함수.
*/
private fun observeSearchResults() {
placeViewModel.searchResults.observe(
this,
) { places ->
(binding.searchResultRecyclerView.adapter as? ResultRecyclerViewAdapter)?.submitList(places)
}
}

/**
* 저장된 검색어를 관찰하고, RecyclerView에 결과를 반영하는 함수.
*/
private fun observeSavedSearchWords() {
savedSearchWordViewModel.savedSearchWords.observe(
this,
) { savedSearchWords ->
(binding.savedSearchWordRecyclerView.adapter as? SavedSearchWordRecyclerViewAdapter)?.submitList(savedSearchWords)
}
}
}
Loading