diff --git a/README.md b/README.md index df2a6477..b65d31b8 100644 --- a/README.md +++ b/README.md @@ -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 활용) \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9932d6bb..cd686279 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bca2f54..b4e7223d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ android:theme="@style/Theme.Map" tools:targetApi="31"> diff --git a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt deleted file mode 100644 index 95b43803..00000000 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package campus.tech.kakao.map - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/campus/tech/kakao/map/models/InitialDataset.kt b/app/src/main/java/campus/tech/kakao/map/models/InitialDataset.kt new file mode 100644 index 00000000..ef846ea5 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/models/InitialDataset.kt @@ -0,0 +1,27 @@ +package campus.tech.kakao.map.models + +object InitialDataset { + val initialData:List = 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", "음식점") + ) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/models/SearchDbHelper.kt b/app/src/main/java/campus/tech/kakao/map/models/SearchDbHelper.kt new file mode 100644 index 00000000..3432f562 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/models/SearchDbHelper.kt @@ -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 { + val result = mutableListOf() + + 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 { + val result = mutableListOf() + + 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 { + val db = readableDatabase + val cursor = db.rawQuery("SELECT * FROM ${SearchResultContract.TABLE_NAME}", null) + + return getAllSearchResultFromCursor(cursor) + } + + fun querySearchResultsByName(name: String): List { + 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 { + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/models/SearchKeywordRepository.kt b/app/src/main/java/campus/tech/kakao/map/models/SearchKeywordRepository.kt new file mode 100644 index 00000000..edbc1cb8 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/models/SearchKeywordRepository.kt @@ -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> = MutableLiveData(listOf()) + val keywords: LiveData> + 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) + } + } + + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/models/SearchResultRepository.kt b/app/src/main/java/campus/tech/kakao/map/models/SearchResultRepository.kt new file mode 100644 index 00000000..9ab064bb --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/models/SearchResultRepository.kt @@ -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> = MutableLiveData(listOf()) + val searchResult: LiveData> + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/models/contracts/SearchKeywordContract.kt b/app/src/main/java/campus/tech/kakao/map/models/contracts/SearchKeywordContract.kt new file mode 100644 index 00000000..c8ee2dbe --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/models/contracts/SearchKeywordContract.kt @@ -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" +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/models/contracts/SearchResultContract.kt b/app/src/main/java/campus/tech/kakao/map/models/contracts/SearchResultContract.kt new file mode 100644 index 00000000..b5de6da0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/models/contracts/SearchResultContract.kt @@ -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" +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view_models/SearchActivityViewModel.kt b/app/src/main/java/campus/tech/kakao/map/view_models/SearchActivityViewModel.kt new file mode 100644 index 00000000..ef9964a1 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view_models/SearchActivityViewModel.kt @@ -0,0 +1,57 @@ +package campus.tech.kakao.map.view_models + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import campus.tech.kakao.map.models.SearchKeywordRepository +import campus.tech.kakao.map.models.SearchResult +import campus.tech.kakao.map.models.SearchResultRepository + + +class SearchActivityViewModel (application: Application): AndroidViewModel(application) { + private val searchResultRepository: SearchResultRepository = SearchResultRepository.getInstance(application) + private val keywordRepository: SearchKeywordRepository = SearchKeywordRepository.getInstance(application) + + private val _searchText: MutableLiveData = MutableLiveData("") + + val searchResult: LiveData> + get() = searchResultRepository.searchResult + val keywords: LiveData> + get() = keywordRepository.keywords + val searchText: LiveData + get() = _searchText + + init{ + keywordRepository.getKeywords() + } + + private fun search(query: String){ + searchResultRepository.search(query) + } + + private fun addKeyword(keyword: String){ + keywordRepository.addKeyword(keyword) + } + + private fun deleteKeyword(keyword: String){ + keywordRepository.deleteKeyword(keyword) + } + + fun clickSearchResultItem(selectedItem: SearchResult){ + addKeyword(selectedItem.name) + } + + fun submitQuery(value: String){ + search(value) + } + + fun clickKeywordDeleteButton(keyword: String){ + deleteKeyword(keyword) + } + + fun clickKeyword(keyword: String){ + _searchText.postValue(keyword) + search(keyword) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/views/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/views/MainActivity.kt new file mode 100644 index 00000000..0f51d3f1 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/views/MainActivity.kt @@ -0,0 +1,86 @@ +package campus.tech.kakao.map.views + +import android.os.Bundle +import android.view.LayoutInflater +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentContainerView +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.view_models.SearchActivityViewModel +import campus.tech.kakao.map.views.adapters.SearchKeywordAdapter + +class MainActivity : AppCompatActivity() { + private lateinit var searchResultFragmentContainer: FragmentContainerView + private lateinit var searchInput: SearchView + private lateinit var keywordRecyclerView: RecyclerView + private val searchViewModel: SearchActivityViewModel by viewModels() + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + initiateViews() + + setInitialValueToAdapter() + initiateLiveDataObservation() + } + + private fun initiateLiveDataObservation() { + searchViewModel.searchText.observe(this) { + searchInput.setQuery(it, false) + } + searchViewModel.keywords.observe(this) { + (keywordRecyclerView.adapter as? SearchKeywordAdapter)?.updateKeywords(it.asReversed()) + setKeywordRecyclerViewActive(it.isNotEmpty()) + } + } + + private fun setInitialValueToAdapter() { + searchViewModel.keywords.value?.let { + (keywordRecyclerView.adapter as? SearchKeywordAdapter)?.updateKeywords(it) + } + } + + private fun initiateSaveKeywordRecyclerView() { + val adapter = SearchKeywordAdapter(LayoutInflater.from(this), { + searchViewModel.clickKeyword(it) + }, { + searchViewModel.clickKeywordDeleteButton(it) + }) + + keywordRecyclerView.adapter = adapter + keywordRecyclerView.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + } + + private fun setKeywordRecyclerViewActive(active: Boolean) { + keywordRecyclerView.isVisible = active + } + + private fun initiateSearchView(){ + + searchInput.setOnQueryTextListener(object: SearchView.OnQueryTextListener{ + override fun onQueryTextSubmit(query: String?): Boolean { + searchViewModel.submitQuery(query?:"") + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + return false + } + }) + } + + private fun initiateViews() { + searchInput = findViewById(R.id.input_search) + keywordRecyclerView = findViewById(R.id.saved_search_bar) + searchResultFragmentContainer = findViewById(R.id.fragment_container_search_result) + keywordRecyclerView = findViewById(R.id.saved_search_bar) + initiateSearchView() + initiateSaveKeywordRecyclerView() + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/views/SearchResultFragment.kt b/app/src/main/java/campus/tech/kakao/map/views/SearchResultFragment.kt new file mode 100644 index 00000000..8da28f3a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/views/SearchResultFragment.kt @@ -0,0 +1,71 @@ +package campus.tech.kakao.map.views + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.models.SearchResult +import campus.tech.kakao.map.view_models.SearchActivityViewModel +import campus.tech.kakao.map.views.adapters.SearchResultAdapter + +class SearchResultFragment : Fragment() { + private lateinit var searchResultRecyclerView: RecyclerView + private lateinit var noResultHelpText: View + private val viewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_search_result, container, false) + } + + private fun setInitialValueToAdapter() { + viewModel.searchResult.value?.let { + (searchResultRecyclerView.adapter as? SearchResultAdapter)?.updateResult(it) + } + } + + private fun initiateRecyclerView(view: View) { + searchResultRecyclerView = view.findViewById(R.id.list_search_result) + searchResultRecyclerView.adapter = + SearchResultAdapter { item: SearchResult, _: Int -> + viewModel.clickSearchResultItem(item) + } + searchResultRecyclerView.layoutManager = + LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false) + searchResultRecyclerView.addItemDecoration( + DividerItemDecoration( + activity, + DividerItemDecoration.VERTICAL + ) + ) + } + + private fun initiateSearchResultLiveDataObservation() { + viewModel.searchResult.observe(viewLifecycleOwner) { + (searchResultRecyclerView.adapter as? SearchResultAdapter)?.updateResult(it) + setNoResultHelpTextActive(it.isEmpty()) + } + } + + private fun setNoResultHelpTextActive(active: Boolean){ + noResultHelpText.isVisible = active + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + noResultHelpText = view.findViewById(R.id.text_no_result) + + initiateRecyclerView(view) + initiateSearchResultLiveDataObservation() + setInitialValueToAdapter() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/views/adapters/SearchKeywordAdapter.kt b/app/src/main/java/campus/tech/kakao/map/views/adapters/SearchKeywordAdapter.kt new file mode 100644 index 00000000..2d41e406 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/views/adapters/SearchKeywordAdapter.kt @@ -0,0 +1,68 @@ +package campus.tech.kakao.map.views.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R + +class KeywordDiffUtil(private val oldList: List, private val newList: List): 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] + +} + +class SearchKeywordAdapter( + private val inflater: LayoutInflater, + val onItemClicked: ((item: String) -> Unit), + val onItemDeleteButtonClicked: ((item: String) -> Unit) +) : RecyclerView.Adapter() { + private var items: List = listOf() + + class SearchKeywordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var nameText: TextView + val deleteText: View + + init { + nameText = itemView.findViewById(R.id.text_search_keyword) + deleteText = itemView.findViewById(R.id.view_delete_keyword) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchKeywordViewHolder { + val view = inflater.inflate(R.layout.item_search_keyword, parent, false) + val holder = SearchKeywordViewHolder(view) + view.setOnClickListener { + onItemClicked(holder.nameText.text.toString()) + } + holder.deleteText.setOnClickListener { + onItemDeleteButtonClicked(holder.nameText.text.toString()) + } + return holder + } + + override fun onBindViewHolder(holder: SearchKeywordViewHolder, position: Int) { + val item = items[position] + holder.nameText.text = item + } + + override fun getItemId(position: Int): Long = position.toLong() + override fun getItemCount(): Int = items.size + + fun updateKeywords(keywords: List) { + val diffUtil = KeywordDiffUtil(items, keywords) + val diffResult = DiffUtil.calculateDiff(diffUtil) + items = keywords + diffResult.dispatchUpdatesTo(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/views/adapters/SearchResultAdapter.kt b/app/src/main/java/campus/tech/kakao/map/views/adapters/SearchResultAdapter.kt new file mode 100644 index 00000000..e8475b37 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/views/adapters/SearchResultAdapter.kt @@ -0,0 +1,84 @@ +package campus.tech.kakao.map.views.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.models.SearchResult + +class SearchResultDiffUtil( + private val oldList: List, + private val newList: List +) : DiffUtil.Callback() { + override fun getOldListSize(): Int = oldList.size + + override fun getNewListSize(): Int = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + return (oldItem.type == newItem.type && oldItem.address == newItem.address && oldItem.name == newItem.name) + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + return (oldItem.type == newItem.type && oldItem.address == newItem.address && oldItem.name == newItem.name) + } + +} + +class SearchResultAdapter( + val onItemClicked: ((item: SearchResult, index: Int) -> Unit) +) : + RecyclerView.Adapter() { + class SearchResultViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val resultName: TextView + val resultAddress: TextView + val resultType: TextView + + init { + resultName = itemView.findViewById(R.id.text_result_name) + resultAddress = itemView.findViewById(R.id.text_result_address) + resultType = itemView.findViewById(R.id.text_result_type) + } + } + + var results: List = listOf() + + override fun getItemCount(): Int =results.size + + fun updateResult(results: List) { + val diffUtil = SearchResultDiffUtil(this.results, results) + val diffResult = DiffUtil.calculateDiff(diffUtil) + this.results = results + diffResult.dispatchUpdatesTo(this) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchResultViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_search_result, parent, false) + val holder = SearchResultViewHolder(view) + view.setOnClickListener { + onItemClicked( + SearchResult( + holder.resultName.text.toString(), + holder.resultAddress.text.toString(), + holder.resultType.text.toString() + ), + holder.bindingAdapterPosition + ) + } + return holder + } + + + override fun onBindViewHolder(holder: SearchResultViewHolder, position: Int) { + val item = results[position] + holder.resultAddress.text = item.address + holder.resultName.text = item.name + holder.resultType.text = item.type + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/map_pin.png b/app/src/main/res/drawable/map_pin.png new file mode 100644 index 00000000..50e1a72a Binary files /dev/null and b/app/src/main/res/drawable/map_pin.png differ diff --git a/app/src/main/res/drawable/x_icon.png b/app/src/main/res/drawable/x_icon.png new file mode 100644 index 00000000..fb039124 Binary files /dev/null and b/app/src/main/res/drawable/x_icon.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 24d17df2..347d5fb0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,15 +5,38 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity"> + tools:context=".views.MainActivity"> - + + + + diff --git a/app/src/main/res/layout/fragment_search_result.xml b/app/src/main/res/layout/fragment_search_result.xml new file mode 100644 index 00000000..df6fd9fc --- /dev/null +++ b/app/src/main/res/layout/fragment_search_result.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_keyword.xml b/app/src/main/res/layout/item_search_keyword.xml new file mode 100644 index 00000000..9c465b62 --- /dev/null +++ b/app/src/main/res/layout/item_search_keyword.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_result.xml b/app/src/main/res/layout/item_search_result.xml new file mode 100644 index 00000000..3db8b8a4 --- /dev/null +++ b/app/src/main/res/layout/item_search_result.xml @@ -0,0 +1,55 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5ba5b9c..3fb64aff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,7 @@ Map + 검색어를 입력해 주세요. + + Hello blank fragment + 검색 결과가 없습니다. \ No newline at end of file