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 #46

Open
wants to merge 45 commits into
base: ksc1008
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
70bd3e5
docs: Init README.md
ksc1008 Jul 1, 2024
3ea0b44
refactor: setup MVVM directories
ksc1008 Jul 1, 2024
6aaaa3b
layout: setup base layout (mainActivity, SearchResultFragment)
ksc1008 Jul 1, 2024
5b027bc
refactor: renamed package names according to the convention
ksc1008 Jul 1, 2024
6539fae
feat: Add SearchDBHelper, SearchResultContract
ksc1008 Jul 1, 2024
dd540d9
feat: now db adds initial data from predefined dataset
ksc1008 Jul 2, 2024
a1f1dc2
feat: add method to print all data from database
ksc1008 Jul 2, 2024
e6c879f
docs: updated README.md
ksc1008 Jul 2, 2024
394cc16
feat: added SearchResultRepository
ksc1008 Jul 2, 2024
b1d384d
fix: used postValue method instead of directly assigning to liveData …
ksc1008 Jul 2, 2024
9b587f0
feat: Add SearchActivityViewModel
ksc1008 Jul 2, 2024
2399d3b
feat: implement SearchResultFragment
ksc1008 Jul 2, 2024
1da4ecc
docs: update README.md for further requirements
ksc1008 Jul 3, 2024
1790b38
docs: fixed typo
ksc1008 Jul 3, 2024
86cee37
feat: add SearchKeywordContract, renamed method names to prevent conf…
ksc1008 Jul 3, 2024
d268733
feat: add SearchKeywordRepository
ksc1008 Jul 3, 2024
2f88f73
feat: add keyword modifying methods to SearchActivityViewModel
ksc1008 Jul 3, 2024
878f270
refactor($SearchResultFragment): separated several procedures to indi…
ksc1008 Jul 3, 2024
f66c191
refactor($MainActivity): separated LiveDataObservation procedure to i…
ksc1008 Jul 3, 2024
6d84183
fix($SearchActivityViewModel): now viewModel do not add keyword on se…
ksc1008 Jul 3, 2024
95a206a
feat: Added onClickListener to viewHolders of recyclerView
ksc1008 Jul 3, 2024
5e8b36f
feat: Added SearchKeywordList in MainActivity
ksc1008 Jul 3, 2024
ba601de
fix: now get all data from db on startup
ksc1008 Jul 3, 2024
21db596
fix: prevented redundant keyword to be added to DB
ksc1008 Jul 3, 2024
b2d2429
refactor: moved Adapter classes to "adapters" package
ksc1008 Jul 3, 2024
7d2e743
feat: added keyword delete button listener
ksc1008 Jul 3, 2024
32e03a8
refactor: refactored views and viewModels to make them less coupled
ksc1008 Jul 3, 2024
950b274
refactor: minor reformatting
ksc1008 Jul 3, 2024
3dd0e79
layout($SearchResultFragment): added divider, added no result text
ksc1008 Jul 3, 2024
e612ef2
refactor($MainActivity): renamed 'savedKeywordListView' to 'keywordRe…
ksc1008 Jul 3, 2024
b4b9928
layout($MainActivity): now keywordRecyclerView automatically collapse…
ksc1008 Jul 3, 2024
3088c7f
refactor: changed all exposed LiveData property to immutable wrapping…
ksc1008 Jul 3, 2024
1d88f49
layout($MainActivity): changed type of searchInput from 'EditText' to…
ksc1008 Jul 3, 2024
f70f161
feat: attached DiffUtils to recyclerView adapters
ksc1008 Jul 5, 2024
88cd01a
refactor: refactored codes for readability
ksc1008 Jul 5, 2024
cf59999
refactor($InitialDataset): changed to object type
ksc1008 Jul 5, 2024
9a44c12
refactor($SearchResultContract): removed index constant values
ksc1008 Jul 5, 2024
6a01110
refactor($SearchDbHelper): renamed "datas" to "dataList"
ksc1008 Jul 5, 2024
9453384
refactor($MainActivityLayout): changed 'Left', 'Right' notation to 'S…
ksc1008 Jul 5, 2024
7bc7521
Merge branch 'step1' into step2
ksc1008 Jul 5, 2024
47c70c6
fix($SearchDbHelper): fixed import compile error
ksc1008 Jul 5, 2024
2b4c664
refactor: changed '!!' notation to 'as Type'
ksc1008 Jul 5, 2024
a674dc1
refactor: changed 'left', 'right' notation to 'start', 'end'
ksc1008 Jul 5, 2024
ec59f2d
refactor($SearchKeywordContract): remove index constant values
ksc1008 Jul 5, 2024
07ff8a6
refactor(SearchResultAdapter): now get layout inflater using parent v…
ksc1008 Jul 8, 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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
# android-map-keyword

## 구현 기능 목록
1. MVVM 아키텍처 구조 설립
2. 기본 레이아웃 구현
3. SQLite 활용하여 데이터베이스 구축
4. 로컬 데이터베이스에 데이터 생성

## Step 2 구현 기능 목록
- [x] SQLiteOpenHelper을 통해 DB를 업데이트 하거나 요청받은 LiveData 데이터를 가져오는 Repository 클래스 구현
- [x] 유저 입력에 따라 Repository로 검색 결과 데이터 조회를 요청하는 ViewModel 클래스 구현
- [x] 검색 결과 Fragment에 리스트를 출력하는 RecyclerView 추가
- [x] RecyclerView Adapter에서 ViewModel의 데이터를 Observe하여 ViewModel의 검색 결과 데이터의 변화에 따라 리스트의 값을 갱신하도록 구현
- [x] SearchInput 에서 텍스트가 변화할 때마다 ViewModel에서 검색 결과를 갱신하도록 구현
- [ ] SearchKeyword 테이블 스키마 및 Contract 생성
- [ ] SearchKeyword 테이블 관련 헬퍼 메소드 작성
- [ ] Keyword를 저장하는 레포지토리 클래스 생성
- [ ] 검색 바 아래 KeywordList 생성
- [ ] 검색 바 Text값을 ViewModel에 바인딩
- [ ] ViewModel에 Keyword 클릭 시 해당 검색어 검색하는 메소드 생성
- [ ] ViewModel에 Keyword 제거하는 메소드 생성
- [ ] ViewModel의 검색 메소드에 KeywordRepository로 새로운 Keyword 저장하는 기능 추가
- [ ] MainActivity에서 Keyword 목록을 Observe하여 자동으로 갱신되는 KeywordList Layout 추가 (ListView 활용)
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ android {

dependencies {

implementation ("androidx.activity:activity-ktx:1.2.2")
implementation ("androidx.fragment:fragment-ktx:1.3.3")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
Expand Down
2 changes: 1 addition & 1 deletion 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=".views.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
11 changes: 0 additions & 11 deletions app/src/main/java/campus/tech/kakao/map/MainActivity.kt

This file was deleted.

27 changes: 27 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/models/InitialDataset.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package campus.tech.kakao.map.models

object InitialDataset {
val initialData:List<SearchResult> = listOf(
SearchResult("카페1", "대구 달서구 복현동 1", "카페"),
SearchResult("카페2", "대구 달서구 복현동 2", "카페"),
SearchResult("카페3", "대구 달서구 복현동 3", "카페"),
SearchResult("카페4", "대구 달서구 복현동 4", "카페"),
SearchResult("카페5", "대구 달서구 복현동 5", "카페"),
SearchResult("카페6", "대구 달서구 복현동 6", "카페"),
SearchResult("카페7", "대구 달서구 복현동 7", "카페"),
SearchResult("음식점1", "대구 달서구 송현동 1", "음식점"),
SearchResult("음식점2", "대구 달서구 송현동 2", "음식점"),
SearchResult("음식점3", "대구 달서구 송현동 3", "음식점"),
SearchResult("음식점4", "대구 달서구 송현동 4", "음식점"),
SearchResult("음식점5", "대구 달서구 송현동 5", "음식점"),
SearchResult("음식점6", "대구 달서구 송현동 6", "음식점"),
SearchResult("음식점7", "대구 달서구 송현동 7", "음식점"),
SearchResult("음식점8", "대구 달서구 송현동 8", "음식점"),
SearchResult("음식점9", "대구 달서구 송현동 9", "음식점"),
SearchResult("음식점10", "대구 달서구 송현동 10", "음식점"),
SearchResult("음식점11", "대구 달서구 송현동 11", "음식점"),
SearchResult("음식점12", "대구 달서구 송현동 12", "음식점"),
SearchResult("음식점13", "대구 달서구 송현동 13", "음식점"),
SearchResult("음식점14", "대구 달서구 송현동 14", "음식점")
)
}
181 changes: 181 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/models/SearchDbHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package campus.tech.kakao.map.models

import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.provider.BaseColumns
import campus.tech.kakao.map.models.contracts.SearchKeywordContract
import campus.tech.kakao.map.models.contracts.SearchResultContract

data class SearchResult(val name: String, val address: String, val type: String)

class SearchDbHelper(context: Context) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase?) {
db?.execSQL(SearchResultContract.CREATE_QUERY)

if (db != null) {
insertInitialData(db)
}
}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
if (oldVersion <= 1) {
db?.execSQL(SearchKeywordContract.CREATE_QUERY)
}
if (oldVersion in 2..2) {
db?.execSQL(SearchKeywordContract.DROP_QUERY)
db?.execSQL(SearchKeywordContract.CREATE_QUERY)
}
}

private fun insertInitialData(db: SQLiteDatabase) {
val dataList = InitialDataset.initialData
for (data in dataList) {
val contentValues = ContentValues().apply {
put(SearchResultContract.COLUMN_NAME, data.name)
put(SearchResultContract.COLUMN_ADDRESS, data.address)
put(SearchResultContract.COLUMN_TYPE, data.type)
}
db.insert(SearchResultContract.TABLE_NAME, null, contentValues)
}
}

fun insertSearchResult(name: String, address: String, type: String) {
val db = writableDatabase
val contentValues = ContentValues().apply {
put(SearchResultContract.COLUMN_NAME, name)
put(SearchResultContract.COLUMN_ADDRESS, address)
put(SearchResultContract.COLUMN_TYPE, type)
}
db.insert(SearchResultContract.TABLE_NAME, null, contentValues)
}

fun insertSearchResult(searchResult: SearchResult) {
insertSearchResult(searchResult.name, searchResult.address, searchResult.type)
}

fun insertOrReplaceKeyword(keyword: String) {
val db = writableDatabase
val contentValues = ContentValues().apply {
put(SearchKeywordContract.COLUMN_KEYWORD, keyword)
}

db.replace(SearchKeywordContract.TABLE_NAME, null, contentValues)
}

fun updateSearchResult(id: String, name: String, address: String, type: String) {
val db = writableDatabase
val contentValues = ContentValues().apply {
put(BaseColumns._ID, id)
put(SearchResultContract.COLUMN_NAME, name)
put(SearchResultContract.COLUMN_ADDRESS, address)
put(SearchResultContract.COLUMN_TYPE, type)
}
db.update(
SearchResultContract.TABLE_NAME,
contentValues,
"${BaseColumns._ID} = ?",
arrayOf(id)
)
}

fun updateSearchResult(id: String, searchResult: SearchResult) {
updateSearchResult(id, searchResult.name, searchResult.address, searchResult.type)
}

fun deleteSearchResult(id: String) {
val db = writableDatabase
db.delete(SearchResultContract.TABLE_NAME, "${BaseColumns._ID} = ?", arrayOf(id))
}

fun deleteKeyword(keyword: String) {
val db = writableDatabase
db.delete(
SearchKeywordContract.TABLE_NAME,
"${SearchKeywordContract.COLUMN_KEYWORD} = ?",
arrayOf(keyword)
)
}

private fun getAllSearchResultFromCursor(cursor: Cursor?): List<SearchResult> {
val result = mutableListOf<SearchResult>()

try {
while (cursor?.moveToNext() == true) {
result.add(
SearchResult(
cursor.getString(1),
cursor.getString(2),
cursor.getString(3)
)
)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (cursor != null && !cursor.isClosed) {
cursor.close()
}
}

return result
}

private fun getAllSearchKeywordFromCursor(cursor: Cursor?): List<String> {
val result = mutableListOf<String>()

try {
while (cursor?.moveToNext() == true) {
result.add(cursor.getString(1))
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (cursor != null && !cursor.isClosed) {
cursor.close()
}
}

return result
}

fun queryAllSearchResults(): List<SearchResult> {
val db = readableDatabase
val cursor = db.rawQuery("SELECT * FROM ${SearchResultContract.TABLE_NAME}", null)

return getAllSearchResultFromCursor(cursor)
}

fun querySearchResultsByName(name: String): List<SearchResult> {
val db = readableDatabase
val cursor = db.rawQuery(
"SELECT * FROM ${SearchResultContract.TABLE_NAME} WHERE ${SearchResultContract.COLUMN_NAME} LIKE \"%$name%\"",
null
)

return getAllSearchResultFromCursor(cursor)
}

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

return getAllSearchKeywordFromCursor(cursor)
}

companion object {
private var instance: SearchDbHelper? = null
const val DATABASE_VERSION = 3
const val DATABASE_NAME = "MapSearch"

fun getInstance(context: Context): SearchDbHelper {
if (instance == null) {
instance = SearchDbHelper(context)
}
return instance as SearchDbHelper
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package campus.tech.kakao.map.models

import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class SearchKeywordRepository(context: Context) {
private val _keywords: MutableLiveData<List<String>> = MutableLiveData(listOf())
val keywords: LiveData<List<String>>
get() = _keywords

private lateinit var searchDb: SearchDbHelper
init {
searchDb = SearchDbHelper(context)
}

fun addKeyword(keyword: String) {
CoroutineScope(Dispatchers.IO).launch {
searchDb.insertOrReplaceKeyword(keyword)
val newData = searchDb.queryAllSearchKeywords()
_keywords.postValue(newData)
Comment on lines +23 to +24
Copy link

Choose a reason for hiding this comment

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

반복되는 동작같은데 메서드를 따로 빼도 괜찮을것 같습니다

}
}

fun deleteKeyword(keyword: String) {
CoroutineScope(Dispatchers.IO).launch {
searchDb.deleteKeyword(keyword)
val newData = searchDb.queryAllSearchKeywords()
_keywords.postValue(newData)
}
}

fun getKeywords() {
CoroutineScope(Dispatchers.IO).launch {
val newData = searchDb.queryAllSearchKeywords()
_keywords.postValue(newData)
}
}

companion object {
private var instance: SearchKeywordRepository? = null

fun getInstance(context: Context): SearchKeywordRepository {
if (instance == null) {
instance = SearchKeywordRepository(context)
}
return instance as SearchKeywordRepository
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package campus.tech.kakao.map.models

import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class SearchResultRepository(context: Context) {
private val _searchResult: MutableLiveData<List<SearchResult>> = MutableLiveData(listOf())
val searchResult: LiveData<List<SearchResult>>
get() = _searchResult

private lateinit var searchDb: SearchDbHelper

init {
searchDb = SearchDbHelper(context)
}

fun search(text: String) {
CoroutineScope(Dispatchers.IO).launch {
val result = searchDb.querySearchResultsByName(text)
_searchResult.postValue(result)
}
}

companion object {
private var instance: SearchResultRepository? = null

fun getInstance(context: Context): SearchResultRepository {
if (instance == null) {
instance = SearchResultRepository(context)
}
return instance as SearchResultRepository
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package campus.tech.kakao.map.models.contracts

import android.provider.BaseColumns

object SearchKeywordContract : BaseColumns {
const val TABLE_NAME = "SEARCH_KEYWORD"
const val COLUMN_KEYWORD = "keyword"

const val CREATE_QUERY = "CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
"${BaseColumns._ID} INTEGER PRIMARY KEY AUTOINCREMENT, " +
"$COLUMN_KEYWORD TEXT," +
"UNIQUE($COLUMN_KEYWORD))"

const val DROP_QUERY = "DROP TABLE IF EXISTS $TABLE_NAME"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package campus.tech.kakao.map.models.contracts

import android.provider.BaseColumns._ID

object SearchResultContract {
const val TABLE_NAME = "SEARCH_RESULT"
const val COLUMN_NAME = "name"
const val COLUMN_ADDRESS = "address"
const val COLUMN_TYPE = "type"

const val CREATE_QUERY = "CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
"$_ID INTEGER PRIMARY KEY AUTOINCREMENT, " +
"$COLUMN_NAME TEXT, " +
"$COLUMN_ADDRESS TEXT, " +
"$COLUMN_TYPE TEXT)"

const val DROP_QUERY = "DROP TABLE IF EXISTS $TABLE_NAME"
}
Loading