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주차 과제_Step2 #43

Open
wants to merge 24 commits into
base: jsh00325
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
381b1ee
docs: Add Program Description & Step1 Feature
jsh00325 Jul 1, 2024
e53a1be
feat: Add Location Search Layout
jsh00325 Jul 2, 2024
cde4321
feat: Add Contract & DbHelper
jsh00325 Jul 2, 2024
5f158fb
feat: Create Dummy Data on Database
jsh00325 Jul 2, 2024
e1238e3
docs: Update Step2 Feature
jsh00325 Jul 4, 2024
d90c483
feat: Add Search History Item Layout
jsh00325 Jul 4, 2024
3c3b587
refactor: Apply MVVM Pattern
jsh00325 Jul 4, 2024
23c102a
feat: Data binding & connect ViewModel, Model
jsh00325 Jul 4, 2024
43533ef
feat: When Click X Button, Clear EditText's text
jsh00325 Jul 4, 2024
0c2bd06
feat: Get Data Matching Category from SQLite
jsh00325 Jul 4, 2024
788e357
feat: Add RecyclerView Adapter for searchResultRecyclerView
jsh00325 Jul 4, 2024
77958d0
feat: Show the Searched Information to RecyclerView
jsh00325 Jul 4, 2024
14ca317
feat: Add Contract & DbHelper for History
jsh00325 Jul 4, 2024
c1e84b5
feat: Add a Method to Check if History exist
jsh00325 Jul 4, 2024
00a9cb3
feat: Add a Method to Remove Row in DB
jsh00325 Jul 4, 2024
457e5b7
feat: Add History When Click Location Item
jsh00325 Jul 4, 2024
48e9578
feat: Add a Method to Get History from SQLite
jsh00325 Jul 5, 2024
f097dce
feat: Update History List When Start & Add Item
jsh00325 Jul 5, 2024
94f989b
feat: Add RecyclerView Adapter for searchHistoryRecyclerView
jsh00325 Jul 5, 2024
7e5040b
feat: Show History to searchHistoryRecyclerView
jsh00325 Jul 5, 2024
007b136
feat: Change private to public at removeHistory()
jsh00325 Jul 5, 2024
76ca1eb
feat: Remove History When Click History Item
jsh00325 Jul 5, 2024
426fcb6
feat: Use DiffUtil to Notify Changed Item in HistoryAdapter
jsh00325 Jul 5, 2024
866fcfa
style: Reformat code
jsh00325 Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
# android-map-keyword

## 📄 프로그램 설명

카카오맵의 클론 코딩을 위해, 검색 레이아웃을 제작하였습니다.

이때 내부 저장소인 SQLite를 통해서 데이터를 검색하고, 기록을 저장하는 기능을 구현합니다.

## 🎯 1단계(로컬 데이터) 구현할 기능

- [X] 검색어를 입력 받고 보여주는 레이아웃 제작

- [X] 검색어를 입력 받는 EditText 제작

- [X] X 버튼을 누르면 검색어를 초기화할 수 있도록 ImageButton 제작

- [X] 검색 결과를 보여주는 레이아웃 제작

- [X] 이전의 검색 기록을 확인할 수 있는 레이아웃 제작

- [X] 검색에 사용될 데이터를 생성하여 SQLite에 저장

## 🎯 2단계(검색) 구현할 기능

- [X] X버튼 클릭 시, 입력된 검색어를 지우기

- [X] 검색어를 입력하면 검색 결과를 띄워 주기

- [X] 업종(`COLUMN_CATEGORY`)과 일치한 결과를 DB에서 리스트로 가져오기

- [X] 가져온 리스트를 RecyclerView에 적용하여 보여주기

- [X] 검색 결과 중 하나를 클릭하는 경우, 해당 장소를 검색 기록에 추가하기

- [X] 검색 기록을 보여주는 item의 레이아웃 제작하기

- [X] 이미 DB에 존재한다면, 해당 값을 지우고 새로 저장해서 맨 뒤로 가도록 구현하기

- [X] DB에 존재하지 않는다면, 새롭게 추가하고 맨 뒤에서 불러오기

- [X] 검색 기록의 X버튼 클릭 시, 해당 검색 기록을 삭제하기
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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.activity:activity:1.8.0")
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
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
android:theme="@style/Theme.Map"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".view.SearchLocationActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand All @@ -23,4 +23,4 @@
</activity>
</application>

</manifest>
</manifest>
11 changes: 0 additions & 11 deletions app/src/main/java/campus/tech/kakao/map/MainActivity.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package campus.tech.kakao.map.model

import android.provider.BaseColumns

object HistoryContract : BaseColumns {
const val TABLE_NAME = "HISTORY"
const val COLUMN_NAME = "name"
}
27 changes: 27 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/model/HistoryDbHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package campus.tech.kakao.map.model

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class HistoryDbHelper(context: Context) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {

override fun onCreate(db: SQLiteDatabase?) {
db?.let {
val query = "CREATE TABLE ${HistoryContract.TABLE_NAME} (" +
"${HistoryContract.COLUMN_NAME} VARCHAR(50))"
it.execSQL(query)
}
}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
db?.execSQL("DROP TABLE IF EXISTS ${HistoryContract.TABLE_NAME}")
Copy link

Choose a reason for hiding this comment

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

db 버전이 업그레이드될때 실행되는 마이그레이션 작업인데
지금은 모든 데이터를 지우고 새로 설정 해주시고 계시네요 ㅎㅎ
지금은 실 서비스가 아니니까 상관 없는데
실제 서비스를 할땐 이런 작업을 유의해야합니다.
앱 업데이트를 했는데 내가 저장해둔 데이터가 날아가면... 무수히 많은 문의를 받을수도 있거든요

Copy link
Author

Choose a reason for hiding this comment

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

"안드로이드 DB(데이터베이스) onUpgrade", tistory / "SQLiteOpenHelper", Android Developers

관련해서 여러 문서들과 블로그들을 찾아보면서, 실제로 onUpgrade()가 호출되는 상황을 이해할 수 있었습니다.
앞으로 업그레이드가 필요한 상황에서는, 기존의 데이터를 새로운 버전으로 마이그레이션을 할 수 있도록 코드를 작성하겠습니다!

onCreate(db)
}

companion object {
const val DATABASE_NAME = "history.db"
const val DATABASE_VERSION = 1
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/model/Location.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package campus.tech.kakao.map.model

data class Location(
val name: String,
val address: String,
val category: String
)
10 changes: 10 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/model/LocationContract.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package campus.tech.kakao.map.model

import android.provider.BaseColumns

object LocationContract : BaseColumns {
const val TABLE_NAME = "LOCATION"
const val COLUMN_NAME = "name"
const val COLUMN_ADDRESS = "address"
const val COLUMN_CATEGORY = "category"
}
51 changes: 51 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/model/LocationDbHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package campus.tech.kakao.map.model

import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class LocationDbHelper(context: Context) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase?) {
db?.let {
val query = "CREATE TABLE ${LocationContract.TABLE_NAME} (" +
"${LocationContract.COLUMN_NAME} VARCHAR(50)," +
"${LocationContract.COLUMN_ADDRESS} VARCHAR(50)," +
"${LocationContract.COLUMN_CATEGORY} VARCHAR(20))"
it.execSQL(query)

createLocationData(it)
}
}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
db?.execSQL("DROP TABLE IF EXISTS ${LocationContract.TABLE_NAME}")
onCreate(db)
}

private fun createLocationData(db: SQLiteDatabase) {
// 임의의 카페 데이터 15개 생성
for (i in 1..15) {
val values = ContentValues()
values.put(LocationContract.COLUMN_NAME, "카페$i")
values.put(LocationContract.COLUMN_ADDRESS, "서울 성동구 성수동 $i")
values.put(LocationContract.COLUMN_CATEGORY, "카페")
db.insert(LocationContract.TABLE_NAME, null, values)
}

// 임의의 약국 데이터 15개 생성
for (i in 1..15) {
val values = ContentValues()
values.put(LocationContract.COLUMN_NAME, "약국$i")
values.put(LocationContract.COLUMN_ADDRESS, "서울 강남구 대치동 $i")
values.put(LocationContract.COLUMN_CATEGORY, "약국")
db.insert(LocationContract.TABLE_NAME, null, values)
}
}

companion object {
const val DATABASE_NAME = "location.db"
const val DATABASE_VERSION = 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package campus.tech.kakao.map.model

import android.content.ContentValues
import android.content.Context

class SearchLocationRepository(context: Context) {
private val locationDbHelper: LocationDbHelper = LocationDbHelper(context)
private val historyDbHelper: HistoryDbHelper = HistoryDbHelper(context)

fun searchLocation(category: String): List<Location> {
val db = locationDbHelper.readableDatabase
val searchQuery = "SELECT * FROM ${LocationContract.TABLE_NAME} " +
"WHERE ${LocationContract.COLUMN_CATEGORY} = '$category'"
val cursor = db.rawQuery(searchQuery, null)

val result = mutableListOf<Location>()
while (cursor.moveToNext()) {
result.add(
Location(
name = cursor.getString(cursor.getColumnIndexOrThrow(LocationContract.COLUMN_NAME)),
address = cursor.getString(cursor.getColumnIndexOrThrow(LocationContract.COLUMN_ADDRESS)),
category = cursor.getString(cursor.getColumnIndexOrThrow(LocationContract.COLUMN_CATEGORY))
)
)
}
cursor.close()
db.close()

return result.toList()
}

fun addHistory(locationName: String) {
if (isExistHistory(locationName)) {
removeHistory(locationName)
}

val db = historyDbHelper.writableDatabase
val historyValues = ContentValues()
historyValues.put(HistoryContract.COLUMN_NAME, locationName)
db.insert(HistoryContract.TABLE_NAME, null, historyValues)

db.close()
}

fun getHistory(): List<String> {
val db = historyDbHelper.readableDatabase
val searchQuery = "SELECT * FROM ${HistoryContract.TABLE_NAME}"
val cursor = db.rawQuery(searchQuery, null)

val result = mutableListOf<String>()
while (cursor.moveToNext()) {
result.add(cursor.getString(cursor.getColumnIndexOrThrow(HistoryContract.COLUMN_NAME)))
}

cursor.close()
db.close()
return result.toList()
}

private fun isExistHistory(locationName: String): Boolean {
val db = historyDbHelper.readableDatabase
val searchQuery = "SELECT * FROM ${HistoryContract.TABLE_NAME} " +
"WHERE ${HistoryContract.COLUMN_NAME} = '$locationName'"
val cursor = db.rawQuery(searchQuery, null)

val result = cursor.count > 0
cursor.close()
db.close()

return result
}

fun removeHistory(locationName: String) {
val db = historyDbHelper.writableDatabase
val deleteQuery = "DELETE FROM ${HistoryContract.TABLE_NAME} " +
"WHERE ${HistoryContract.COLUMN_NAME} = '$locationName'"
db.execSQL(deleteQuery)
db.close()
}
}
52 changes: 52 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/view/HistoryAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package campus.tech.kakao.map.view

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import campus.tech.kakao.map.databinding.ItemHistoryBinding
import campus.tech.kakao.map.viewmodel.SearchLocationViewModel

class HistoryAdapter(
private var dataList: List<String>,
private val context: Context,
private val viewModel: SearchLocationViewModel
) : RecyclerView.Adapter<HistoryAdapter.MyViewHolder>() {

inner class MyViewHolder(private val binding: ItemHistoryBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.removeLocationHistoryButton.setOnClickListener {
viewModel.removeHistory(dataList[bindingAdapterPosition])
}
}
fun binding(historyData: String) {
binding.locationHistoryNameTextView.text = historyData
}
}

fun updateDataList(newDataList: List<String>) {
val diffUtil = HistoryDiffUtilCallback(dataList, newDataList)
val diffResult = DiffUtil.calculateDiff(diffUtil)

dataList = newDataList
diffResult.dispatchUpdatesTo(this)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
ItemHistoryBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
)
}

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.binding(dataList[position])
}

override fun getItemCount(): Int = dataList.size
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package campus.tech.kakao.map.view

import androidx.recyclerview.widget.DiffUtil

class HistoryDiffUtilCallback(
private val oldList: List<String>,
private val newList: List<String>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size

override fun getNewListSize(): Int = newList.size

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition] == newList[newItemPosition]

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition] == newList[newItemPosition]
Copy link

Choose a reason for hiding this comment

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

DiffUtil을 사용 해보셨군요 🎉
지금 구현상태로도 적당히 잘 동작하긴 합니다만, 아직 최적화가 되지 않았습니다.
areItemsTheSameareContentsTheSame 구현 때문인데요
이부분 한번 더 연구 해보시겠어요?

Copy link
Author

Choose a reason for hiding this comment

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

areContentsTheSame()은 두 객체간의 내용이 같은 경우를 확인하기 위한 함수이고(공식 문서), areItemsTheSame()은 두 객체의 id값을 비교해서, 정말로 같은 객체인지 확인하기 위한 함수(공식 문서)라고 이해했습니다.
따라서 areItemsTheSame()의 결과가 true인 경우에만 areContentsTheSame()가 호출된다고 이해했습니다.

이 부분을 최적화하기 위해 여러 블로그 글을 봤는데, 대부분의 예제들이 Data Class를 설정한 후 id값을 통해 areItemsTheSame()를 구현하는 것을 확인하였습니다. 이에 반해 현재 제 코드는 String 타입을 비교하는데, 이러한 경우에는 ===연산자를 통해서 비교하면 될까요..?

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
    oldList[oldItemPosition] === newList[newItemPosition]

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
    oldList[oldItemPosition] == newList[newItemPosition]

Copy link

Choose a reason for hiding this comment

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

areContentsTheSame()은 두 객체간의 내용이 같은 경우를 확인하기 위한 함수이고(공식 문서), areItemsTheSame()은 두 객체의 id값을 비교해서, 정말로 같은 객체인지 확인하기 위한 함수(공식 문서)라고 이해했습니다.

정확히 잘 이해하셨습니다 👏
지금처럼 데이터가 String type일 경우엔 id 로 비교가 불가능해서 areItemsTheSame areContentsTheSame 구현이 동일할수밖에 없습니다.
data class를 하나 만드셔서 id 개념을 도입하시면 좀 더 최적화를 해보실수도 있으실거에요

}
Loading