Room 사용하기

  • 라이브러리 추가하기
    // plugins
    id 'kotlin-kapt'

    // dependencies
    kapt 'androidx.room:room-compiler:2.4.2'
    implementation 'androidx.room:room-runtime:2.4.2'


  • 검색 기록 모델 만들기

model 생성

// History.kt
@Entity
data class History(
    @PrimaryKey val uid:Int?,
    @ColumnInfo(name = "keyword") val keyword:String?
):Serializable


  • Dao 생성

dao 패키지를 생성, 그 아래에 파일 생성

// HistoryDao.kt
@Dao
interface HistoryDao {

    @Query("SELECT * FROM history")
    fun getAll() : List<History>

    @Insert
    fun insertHistory(history:History)

    @Query("DELETE FROM history WHERE keyword==:keyword")
    fun delete(keyword:String)
}


  • Database 생성
// AppDatabase.kt
@Database(entities = [History::class], version =1)
abstract class AppDatabase:RoomDatabase() {
    abstract fun historyDao() : HistoryDao
}


  • 메인 엑티비티에서 디비 생성
lateinit var db: AppDatabase

...

initDB()

...

    private fun initDB() {
        db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "BookSearchDB"
        ).build()
    }

    private fun saveSearchKeyword(keyword:String) {
        Thread{
            db.historyDao().insertHistory(History(null, keyword))
        }.start()
    }



  • 검색 기록 리사이클러뷰 생성

tools:listitem = "@layout/id" 사용 시 리스트가 자동으로 채워짐!

평소에 잘 사용해보자

    <!--activity_main.xml 맨 아래(기존 리사이클러뷰 보다 위에 보이도록, 배경을 흰색 지정)-->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/historyRecyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/white"
        android:visibility="gone" 
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/searchEditText"/>


<!--item_hstory.xml-->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/historyTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="8dp"
        android:layout_marginStart="16dp"
        android:textColor="@color/black"
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageButton
        android:id="@+id/historyDeleteButton"
        android:layout_width="12dp"
        android:layout_height="12dp"
        android:layout_marginEnd="16dp"
        android:src="@drawable/ic_baseline_clear_24"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>



  • 히스토르 어뎁터 구현
// 삭제 버튼 구현을 위해서 람다 함수를 변수로 받음
class HistoryAdapter(val historyDeleteCLikedListener: (String) -> Unit):ListAdapter<History, HistoryAdapter.HistoryItemViewHolder>(diffUtil){

    inner class HistoryItemViewHolder(private val binding: ItemHistroyBinding) :RecyclerView.ViewHolder(binding.root) {
        fun bind(historyModel:History){
            binding.historyTextView.text = historyModel.keyword

            binding.historyDeleteButton.setOnClickListener{
                historyDeleteCLikedListener(historyModel.keyword.orEmpty())
            }
        }
    }

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

    override fun onBindViewHolder(holder: HistoryItemViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

    companion object{
        val diffUtil = object: DiffUtil.ItemCallback<History>(){
            override fun areItemsTheSame(oldItem: History, newItem: History): Boolean {
                return oldItem==newItem
            }

            override fun areContentsTheSame(oldItem: History, newItem: History): Boolean {
                return oldItem.keyword == newItem.keyword   
            }

        }
    }
}


  • 메인 엑티비티에서 히스토리 리사이클러뷰 구현
    private fun showHistoryView() {
        Thread {
            val keywords = db.historyDao().getAll().reversed()
            runOnUiThread {
                binding.historyRecyclerView.visibility = View.VISIBLE
                historyAdapter.submitList(keywords)
            }
        }.start()
    }

    private fun hideHistoryView() {
        binding.historyRecyclerView.visibility = View.GONE
    }
  • http 허용

매니페스트 파일에 추가, https 는 암호화 통신, http 는 평문 통신인데 평문 통신을 허용함(위 bookModel.coverSmallUrl 가 http 사용하기 때문)

android:useCleartextTraffic = "true"



검색 기능 추가하기

  • EditText를 1줄로 제한
android:lines="1"


  • EditText.setOnKeyListener 설정(에딧 텍스트의 키가 눌렸을 때)
        binding.searchEditText.setOnKeyListener { v, keyCode, event ->
            if(keyCode == KeyEvent.KEYCODE_ENTER && event.action==KeyEvent.ACTION_DOWN){ // 엔터가 눌리면
                search(binding.)
                return@setOnKeyListener true
            }
            return@setOnKeyListener false
        }


  • 책 검색 api 사용 함수 정의(search())
    private fun search(keyword: String) {
        bookService.getBooksByName(getString(R.string.interparkAPIKey), keyword)
            .enqueue(object : Callback<SearchBookDTO> {

                override fun onResponse(
                    call: Call<SearchBookDTO>,
                    response: Response<SearchBookDTO>
                ) {

                    // 아래 두함수는 곧 정의
                    saveSearchKeyword(keyword)  // 히스토리 저장하기
                    if (response.isSuccessful.not()) {
                        return
                    }
                    response.body()?.let {
                        adapter.submitList(it.books)
                    }
                }

                override fun onFailure(call: Call<SearchBookDTO>, t: Throwable) {
                    Log.e(TAG, t.toString())
                    // 밑에서 구현
                    hideHistoryView()
                }

            })
    }


히스토리 어뎁터 사용

    lateinit var historyAdapter: HistoryAdapter

    ...
    
    initHistoryRecyclerView()
    initSearchEditText()
    
    ...

    private fun initHistoryRecyclerView() {
        historyAdapter = HistoryAdapter { deleteSearchKeyword(it) }
        binding.historyRecyclerView.adapter = historyAdapter
        binding.historyRecyclerView.layoutManager = LinearLayoutManager(this)
    }


    @SuppressLint("ClickableViewAccessibility")
    private fun initSearchEditText() {
        // 키 이벤트 KeyEvent = 타자
        binding.searchEditText.setOnKeyListener { v, keyCode, event ->
            if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN) { // 엔터가 눌리면
                val keyword = (v as EditText).text.toString()
                search(keyword)
                saveSearchKeyword(keyword)
                return@setOnKeyListener true
            }
            return@setOnKeyListener false
        }

        // 모션 이벤트 MotionEvent = 터치
        binding.searchEditText.setOnTouchListener { v, event ->
            if (event.action == MotionEvent.ACTION_DOWN) {
                showHistoryView()
            }
            return@setOnTouchListener false
        }
    }



  • 현재 전체 코드
class MainActivity : AppCompatActivity() {

    //viewBinding 적용
    lateinit var binding: ActivityMainBinding
    lateinit var adapter: BookAdapter
    lateinit var historyAdapter: HistoryAdapter
    lateinit var bookService: BookService
    lateinit var db: AppDatabase

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initDB()
        initSearchEditText()
        initBookRecyclerView()
        initHistoryRecyclerView()
        initRetrofit()
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun initSearchEditText() {
        // 키 이벤트 KeyEvent = 타자
        binding.searchEditText.setOnKeyListener { v, keyCode, event ->
            if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN) { // 엔터가 눌리면
                val keyword = (v as EditText).text.toString()
                search(keyword)
                saveSearchKeyword(keyword)
                return@setOnKeyListener true
            }
            return@setOnKeyListener false
        }

        // 모션 이벤트 MotionEvent = 터치
        binding.searchEditText.setOnTouchListener { v, event ->
            if (event.action == MotionEvent.ACTION_DOWN) {
                showHistoryView()
            }
            return@setOnTouchListener false
        }
    }

    private fun initHistoryRecyclerView() {
        historyAdapter = HistoryAdapter { deleteSearchKeyword(it) }
        binding.historyRecyclerView.adapter = historyAdapter
        binding.historyRecyclerView.layoutManager = LinearLayoutManager(this)
    }

    private fun deleteSearchKeyword(keyword: String) {
        Thread {
            db.historyDao().delete(keyword)
        }.start()
        showHistoryView()
    }

    private fun initDB() {
        db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "BookSearchDB"
        ).build()
    }

    private fun initBookRecyclerView() {
        adapter = BookAdapter()
        binding.bookRecyclerView.adapter = adapter
        binding.bookRecyclerView.layoutManager = LinearLayoutManager(this)
    }

    private fun initRetrofit() {
        val retrofit = Retrofit.Builder()
            .baseUrl("https://book.interpark.com")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        bookService = retrofit.create(BookService::class.java)
        bookService.getBestSeller(getString(R.string.interparkAPIKey))
            .enqueue(object : Callback<BestSellerDTO> {

                override fun onResponse(
                    call: Call<BestSellerDTO>,
                    response: Response<BestSellerDTO>
                ) {
                    if (response.isSuccessful.not()) {
                        return
                    }
                    response.body()?.let {
                        Log.d(TAG, it.toString())
                        it.books.forEach { book ->
                            Log.d(TAG, book.toString())
                        }
                        adapter.submitList(it.books)
                    }
                }

                override fun onFailure(call: Call<BestSellerDTO>, t: Throwable) {
                    Log.e(TAG, t.toString())
                }

            })
    }

    private fun saveSearchKeyword(keyword: String) {
        Thread {
            db.historyDao().insertHistory(History(null, keyword))
        }.start()
    }

    private fun search(keyword: String) {
        bookService.getBooksByName(getString(R.string.interparkAPIKey), keyword)
            .enqueue(object : Callback<SearchBookDTO> {

                override fun onResponse(
                    call: Call<SearchBookDTO>,
                    response: Response<SearchBookDTO>
                ) {

                    hideHistoryView()
                    saveSearchKeyword(keyword)
                    if (response.isSuccessful.not()) {
                        return
                    }
                    response.body()?.let {
                        adapter.submitList(it.books)
                    }
                }

                override fun onFailure(call: Call<SearchBookDTO>, t: Throwable) {
                    Log.e(TAG, t.toString())
                    hideHistoryView()
                }

            })
    }

    private fun showHistoryView() {
        Thread {
            val keywords = db.historyDao().getAll().reversed()
            runOnUiThread {
                historyAdapter.submitList(keywords)
            }
        }.start()
        binding.historyRecyclerView.isVisible=true
    }

    private fun hideHistoryView() {
        binding.historyRecyclerView.isVisible=false
    }

    companion object {
        const val TAG = "MainActivity"
    }
}