-
Notifications
You must be signed in to change notification settings - Fork 33
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주차 과제 Step 2 #47
base: njiyeon
Are you sure you want to change the base?
Changes from 4 commits
47620e9
06bd690
1a73ccb
a24e1ea
7280256
82a1c5b
dadea62
6cb89aa
3a6389d
e032115
9e98ba1
a837a8e
48545ca
ec88b25
0a9b162
a364d15
aced112
56982fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,16 @@ | ||
# android-map-keyword | ||
## 카카오맵 클론 코딩을 위한 시작 | ||
|
||
### step 1 | ||
- 검색어 입력 및 검색 결과를 표시할 기본 레이아웃을 구현한다. | ||
- 검색에 사용될 데이터를 로컬 데이터베이스에 생성한다. | ||
|
||
### step 2 | ||
- 검색어를 입력하면 검색 결과 목록이 표시된다. | ||
- 검색 결과 목록은 세로 스크롤이 된다. | ||
- 입력한 검색어는 X를 눌러서 삭제할 수 있다. | ||
- 검색 결과 목록에서 하나의 항목을 선택할 수 있다. | ||
- 선택된 항목은 검색어 저장 목록에 추가된다. | ||
- 저장된 검색어 목록은 가로 스크롤이 된다. | ||
- 저장된 검색어는 X를 눌러서 삭제할 수 있다. | ||
- 저장된 검색어는 앱을 재실행하여도 유지된다 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package campus.tech.kakao.map | ||
|
||
import android.database.Cursor | ||
import android.view.LayoutInflater | ||
import android.view.ViewGroup | ||
import androidx.recyclerview.widget.RecyclerView | ||
import campus.tech.kakao.map.databinding.ItemHistoryBinding | ||
|
||
class HistoryAdapter(private val onDelete: (String) -> Unit) : RecyclerView.Adapter<HistoryAdapter.ViewHolder>() { | ||
|
||
private var cursor: Cursor? = null | ||
|
||
inner class ViewHolder(private val binding: ItemHistoryBinding) : RecyclerView.ViewHolder(binding.root) { | ||
|
||
fun bind(name: String) { | ||
binding.tvName.text = name | ||
binding.ibDelete.setOnClickListener { | ||
onDelete(name) | ||
} | ||
} | ||
} | ||
|
||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||
val binding = ItemHistoryBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||
return ViewHolder(binding) | ||
} | ||
|
||
override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||
cursor?.apply { | ||
moveToPosition(position) | ||
val name = getString(getColumnIndexOrThrow(HistoryContract.COLUMN_NAME)) | ||
holder.bind(name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제가 잘 안읽히는 이유 중 하나도 apply의 역할때문입니다. apply는 객체의 속성을 정할 때 많이 사용되어서 getString, getColumnIndexOrThrow가 어디에 속한 메서드인지 명확하게 파악하기 힘드네요. |
||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 어떤 로직을 원하신 건가요? |
||
} | ||
|
||
override fun getItemCount(): Int { | ||
return cursor?.count ?: 0 | ||
} | ||
|
||
fun submitCursor(cursor: Cursor?) { | ||
this.cursor = cursor | ||
notifyDataSetChanged() | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package campus.tech.kakao.map | ||
|
||
object HistoryContract { | ||
const val TABLE_NAME = "History" | ||
const val COLUMN_NAME = "name" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package campus.tech.kakao.map | ||
|
||
import android.content.Context | ||
import android.database.sqlite.SQLiteDatabase | ||
import android.database.sqlite.SQLiteOpenHelper | ||
|
||
class HistoryDBHelper( | ||
context: Context?, | ||
name: String?, | ||
factory: SQLiteDatabase.CursorFactory?, | ||
version: Int | ||
) : SQLiteOpenHelper(context, name, factory, version) { | ||
|
||
override fun onCreate(db: SQLiteDatabase?) { | ||
val createTableQuery = "CREATE TABLE ${HistoryContract.TABLE_NAME} (" + | ||
"${HistoryContract.COLUMN_NAME} TEXT)" | ||
db?.execSQL(createTableQuery) | ||
} | ||
|
||
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { | ||
db?.execSQL("DROP TABLE IF EXISTS ${HistoryContract.TABLE_NAME}") | ||
onCreate(db) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,81 @@ | ||
package campus.tech.kakao.map | ||
|
||
import android.os.Bundle | ||
import android.text.Editable | ||
import android.text.TextWatcher | ||
import android.view.View | ||
import androidx.activity.viewModels | ||
import androidx.appcompat.app.AppCompatActivity | ||
import androidx.lifecycle.Observer | ||
import androidx.recyclerview.widget.LinearLayoutManager | ||
import campus.tech.kakao.map.databinding.ActivityMainBinding | ||
|
||
class MainActivity : AppCompatActivity() { | ||
|
||
private lateinit var binding: ActivityMainBinding | ||
private val viewModel: PlaceViewModel by viewModels() | ||
private lateinit var placeAdapter: PlaceAdapter | ||
private lateinit var historyAdapter: HistoryAdapter | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
setContentView(R.layout.activity_main) | ||
binding = ActivityMainBinding.inflate(layoutInflater) | ||
setContentView(binding.root) | ||
|
||
binding.btnDelete.setOnClickListener { | ||
binding.etSearch.setText(null) | ||
} | ||
|
||
setupView() | ||
|
||
viewModel.places.observe(this, Observer { cursor -> | ||
placeAdapter.submitCursor(cursor) | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Data Observing 하는 로직도 분리하면 좋겠네요. |
||
|
||
viewModel.history.observe(this, Observer { cursor -> | ||
historyAdapter.submitCursor(cursor) | ||
}) | ||
|
||
binding.etSearch.addTextChangedListener(object : TextWatcher { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. addTestChangedListener 확장함수 잘 찾아보시면 원하는 함수만 재정의할 수 있습니다. |
||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { | ||
// Do nothing | ||
} | ||
|
||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { | ||
val searchText = s.toString().trim() | ||
if (searchText.isEmpty()) { | ||
binding.tvNoResults.visibility = View.VISIBLE | ||
binding.recyclerViewPlaces.visibility = View.INVISIBLE | ||
} else { | ||
binding.tvNoResults.visibility = View.INVISIBLE | ||
binding.recyclerViewPlaces.visibility = View.VISIBLE | ||
viewModel.searchPlaces(searchText) | ||
} | ||
} | ||
|
||
override fun afterTextChanged(s: Editable?) { | ||
// Do nothing | ||
} | ||
}) | ||
} | ||
|
||
private fun setupView() { | ||
placeAdapter = PlaceAdapter { name -> | ||
viewModel.addHistory(name) | ||
} | ||
|
||
historyAdapter = HistoryAdapter { name -> | ||
viewModel.removeHistory(name) // 삭제 버튼 클릭 시 ViewModel의 removeHistory 메서드 호출 | ||
} | ||
|
||
binding.recyclerViewPlaces.apply { | ||
adapter = placeAdapter | ||
layoutManager = LinearLayoutManager(this@MainActivity) | ||
} | ||
|
||
binding.recyclerViewHistory.apply { | ||
adapter = historyAdapter | ||
layoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.HORIZONTAL, false) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package campus.tech.kakao.map | ||
|
||
import android.database.Cursor | ||
import android.view.LayoutInflater | ||
import android.view.ViewGroup | ||
import androidx.recyclerview.widget.RecyclerView | ||
import campus.tech.kakao.map.databinding.ItemPlaceBinding | ||
|
||
class PlaceAdapter(private val onClick: (String) -> Unit) : RecyclerView.Adapter<PlaceAdapter.ViewHolder>() { | ||
|
||
private var cursor: Cursor? = null | ||
|
||
inner class ViewHolder(private val binding: ItemPlaceBinding) : RecyclerView.ViewHolder(binding.root) { | ||
|
||
fun bind(name: String, address: String, category: String) { | ||
binding.tvName.text = name | ||
binding.tvAddress.text = address | ||
binding.tvCategory.text = category | ||
binding.root.setOnClickListener { | ||
onClick(name) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 클릭 리스너를 bind 때에 작성하는건 Anti-pattern 입니다. 왜 그런지 생각해보시고 리팩토링 해주세요! |
||
} | ||
} | ||
|
||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||
val binding = ItemPlaceBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||
return ViewHolder(binding) | ||
} | ||
|
||
override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||
cursor?.apply { | ||
moveToPosition(position) | ||
val name = getString(getColumnIndexOrThrow(PlaceContract.COLUMN_TITLE)) | ||
val address = getString(getColumnIndexOrThrow(PlaceContract.COLUMN_LOCATION)) | ||
val category = getString(getColumnIndexOrThrow(PlaceContract.COLUMN_CATEGORY)) | ||
holder.bind(name, address, category) | ||
} | ||
} | ||
|
||
override fun getItemCount(): Int { | ||
return cursor?.count ?: 0 | ||
} | ||
|
||
fun submitCursor(cursor: Cursor?) { | ||
this.cursor = cursor | ||
notifyDataSetChanged() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package campus.tech.kakao.map | ||
|
||
object PlaceContract { | ||
const val TABLE_NAME = "places" | ||
const val COLUMN_CATEGORY = "category" | ||
const val COLUMN_TITLE = "title" | ||
const val COLUMN_LOCATION = "location" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package campus.tech.kakao.map | ||
|
||
import android.content.Context | ||
import android.database.sqlite.SQLiteDatabase | ||
import android.database.sqlite.SQLiteOpenHelper | ||
|
||
class PlaceDBHelper(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { | ||
|
||
override fun onCreate(db: SQLiteDatabase?) { | ||
db?.execSQL(SQL_CREATE_TABLE) | ||
insertInitialRecords(db) | ||
} | ||
|
||
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { | ||
db?.execSQL(SQL_DROP_TABLE) | ||
onCreate(db) | ||
} | ||
|
||
private fun insertInitialRecords(db: SQLiteDatabase?) { | ||
db?.beginTransaction() | ||
try { | ||
db?.let { | ||
insertCafeRecords(it) | ||
insertPharmacyRecords(it) | ||
} | ||
db?.setTransactionSuccessful() | ||
} finally { | ||
db?.endTransaction() | ||
} | ||
} | ||
|
||
private fun insertCafeRecords(db: SQLiteDatabase) { | ||
val category = "카페" | ||
val titleBase = "카페" | ||
val locationBase = "서울 성동구 성수동" | ||
|
||
for (i in 1..20) { | ||
val title = "$titleBase$i" | ||
val location = "$locationBase $i" | ||
db.execSQL( | ||
"INSERT INTO ${PlaceContract.TABLE_NAME} (${PlaceContract.COLUMN_CATEGORY}, ${PlaceContract.COLUMN_TITLE}, ${PlaceContract.COLUMN_LOCATION}) VALUES ('$category', '$title', '$location');" | ||
) | ||
} | ||
} | ||
|
||
private fun insertPharmacyRecords(db: SQLiteDatabase) { | ||
val category = "약국" | ||
val titleBase = "약국" | ||
val locationBase = "서울 강남구 대치동" | ||
|
||
for (i in 1..20) { | ||
val title = "$titleBase$i" | ||
val location = "$locationBase $i" | ||
db.execSQL( | ||
"INSERT INTO ${PlaceContract.TABLE_NAME} (${PlaceContract.COLUMN_CATEGORY}, ${PlaceContract.COLUMN_TITLE}, ${PlaceContract.COLUMN_LOCATION}) VALUES ('$category', '$title', '$location');" | ||
) | ||
} | ||
} | ||
|
||
companion object { | ||
private const val DB_VERSION = 1 | ||
private const val DB_NAME = "location.db" | ||
private const val SQL_CREATE_TABLE = | ||
"CREATE TABLE ${PlaceContract.TABLE_NAME} (" + | ||
"${PlaceContract.COLUMN_CATEGORY} TEXT NOT NULL, " + | ||
"${PlaceContract.COLUMN_TITLE} TEXT NOT NULL, " + | ||
"${PlaceContract.COLUMN_LOCATION} TEXT NOT NULL);" | ||
private const val SQL_DROP_TABLE = "DROP TABLE IF EXISTS ${PlaceContract.TABLE_NAME}" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor를 Adapter까지 끌고 들어오는건 좋지 않아요. DB와 연결되는 객체이기 때문에 Adapter의 역할을 한참 벗어난것 같네요. ViewModel 단에서 Item을 load 하고 Adapter는 단순히 불러온 아이템을 표현하는 용도로만 사용해주세요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
쉽게 말하자면 신뢰할 수 있는 데이터의 집합인 LocalDB 에서 Adapter 까지 내려와야 합니다. LiveData 등을 활용해서
LocalDB -> Repository(Optional) -> ViewModel -> View -> Adpater 로 데이터가 흐르게끔 작성해보세요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 이해한 것이 맞다면, Cursor를 Adapter에서 직접 사용하는 게 아니라 ViewModel에서 데이터를 불러와 Adapter에 전달하도록 바꾸면 될까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네 맞습니다 :)