Android Jetpack

책 검색 앱 만들기(4) - 검색결과를 UI에 표시하기

wdadaww 2023. 1. 20. 16:15
  • 이미지를 보여주기위해 Coil 을 사용해보도록하자.

Coil이란? Kotlin Coroutines으로 만들어진 Android 백앤드 이미지 로딩 라이브러리입니다.

Coil : Coruotine image loader의 약자입니다

  • 빠르다: Coil은 메모리와 디스크의 캐싱, 메모리의 이미지 다운 샘플링, Bitmap 재사용, 일시정지/취소의 자동화 등등 수 많은 최적화 작업을 수행합니다.
  • 가볍다: Coil은 최대 2000개의 method들을 APK에 추가합니다(이미 OkHttp와 Coroutines을 사용중인 앱에 한하여), 이는 Picasso 비슷한 수준이며 Glide와 Fresco보다는 적습니다.
  • 사용하기 쉽다: Coil API는 심플함과 최소한의 상용구를 위하여 Kotlin의 기능을 활용합니다.
  • 현대적이다: Coil은 Kotlin 우선이며 Coroutines, OkHttp, Okio, AndroidX Lifecycles등의 최신 라이브러리를 사용합니다.
  • 출처 :코일 (coil-kt.github.io)
 

Coil

Overview An image loading library for Android backed by Kotlin Coroutines. Coil is: Fast: Coil performs a number of optimizations including memory and disk caching, downsampling the image in memory, automatically pausing/cancelling requests, and more. Ligh

coil-kt.github.io

 

  • Coil 라이브러리를 사용하기위해 build.gradle(app) 파일에 추가한다.
implementation "io.coil-kt:coil:2.2.2"

 

  • 책 이미지를 넣을 ui 작성해줍니다.
<?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"
    android:padding="10dp">

    <ImageView
        android:id="@+id/ArticleImage"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:scaleType="fitXY"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/ArticleTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:text="제목"
        android:maxLines="1"
        android:ellipsize="end"
        android:textSize="35dp"
        android:textStyle="italic"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/ArticleImage"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/ArticleAuthor"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:text="저자"
        android:textSize="30dp"
        android:textStyle="italic"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/ArticleImage"
        app:layout_constraintTop_toBottomOf="@id/ArticleTitle"  />

    <TextView
        android:id="@+id/ArticleDateTime"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:text="출판일"
        android:textSize="25dp"
        android:textStyle="italic"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/ArticleImage"
        app:layout_constraintTop_toBottomOf="@id/ArticleAuthor" />

</androidx.constraintlayout.widget.ConstraintLayout>

-> 이 ui와 데이터를 연결시켜주는 뷰홀더 와 어댑터 클래스를 만들어줍니다


package com.example.kakaobook.ui.view.ui.view.adapter

import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup

import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView

import coil.load
import com.example.kakaobook.databinding.ItemBookViewBinding
import com.example.kakaobook.ui.view.data.model.Book

class BookSearchAdapter : ListAdapter<Book, BookSearchAdapter.BookSearchViewHolder>(diffUtil) {
    inner class BookSearchViewHolder(private val binding: ItemBookViewBinding
    ) : RecyclerView.ViewHolder(binding.root) {
        @SuppressLint("SetTextI18n")
        fun bind(book: Book) {
            val publisher = book.publisher
            val author = book.authors.toString().removeSurrounding("[", "]")
            val date = if (book.datetime.isNotEmpty())
                book.datetime.substring(0, 10)
            else ""

            itemView.apply {
                binding.ArticleImage.load(book.thumbnail)
                binding.ArticleTitle.text = book.title
                binding.ArticleAuthor.text = "$author/$publisher"
                binding.ArticleDateTime.text = date
            }
        }
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookSearchViewHolder {
        return BookSearchViewHolder(
            ItemBookViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )
    }
    override fun onBindViewHolder(holder: BookSearchViewHolder, position: Int) {

        holder.bind(currentList[position])
    }
    companion object {
        private val diffUtil = object : DiffUtil.ItemCallback<Book>() {
            override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean {
                return oldItem.isbn == newItem.isbn
            }
            override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean {
                return oldItem == newItem
            }
        }
    }
}



  • ListAdapter 구현 및 부착

 

  • ViewHolder

각각의 뷰를 보관하는 Holder 객체이다.

Item View 들을 재활용하기 위해 각 요소를 저장해 두고 사용한다.

아이템 생성시 뷰 바인딩은 한 번만 하게 되며, 그 바인딩된 객체를 그대로 가져다 사용하여 성능 부분에서 효율성을 올려준다.

  •  RecyclerView의 Adapter

-> 어댑터는 미리 생성해준 뷰홀더 객체에

사용자가 원하는 데이터리스트를 주입하고

데이터 리스트의 변경사항이 있을때 이를 ui에 반영한다.

  • RecyclerView.DiffUtil

-> 현재 데이터 리스와 교체될 데이터 리스를 비교하고,

진짜 바뀌어야 할 데이터만 바꿔줌으로써 훨씬 빠른 시간 내에 효율적으로 데이터 교환을 할수 있게 한다.

 

DiffUtil을 사용하기 위해서 DiffUtil.Callback이라는 기능을 구현해야 한다. 이때 다음과 같은 사항들을 구현하도록 한다.

  • areItemsTheSame : 기존 어댑터와 새롭게 변경되는 어댑터의 아이템이 같은지 확인한다. 각 아이템의 고유 ID값을 활용하여 비교한다. 리턴 타입은 Boolean이다.
  • areContentsTheSame : 기존 어댑터와 변경되는 어댑터의 아이템 안의 내용을 비교한다. areItemsTheSame 에서 true가 나올 경우 추가적으로 비교하기 위해서 사용하는 함수이다.

 


fragment_search ui를 작성해줍니다.

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.view.ui.view.SearchFragment">

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/SearchText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="Search..."
        android:padding="5dp"
        app:endIconMode="clear_text"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/SearchEditText"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:inputType="textAutoComplete"/>


    </com.google.android.material.textfield.TextInputLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/SearchResult"
        android:layout_width="0dp"
        android:layout_height="0dp"

        android:scrollbars="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/SearchText"
        app:layout_constraintVertical_bias="1.0" />


</androidx.constraintlayout.widget.ConstraintLayout>
  • TextInputlayout

바깥에 textInputLayout이 있고, TextInputEditText가 내부에 있다. TextInputEditText가 EditText라고 생각하면 되고, TextInputLayout과 상호작용을 하며 여러 기능을 사용할 수 있다.

또한 hint부분에 Search...에   EditText는 text를 기입하기 위해 edittext를 클릭하면 사라지지만 textinputlayout에 담긴 edittext는 사라짐과 동시에 왼쪽상단에 작게 hint를 표기해준다.


   View - SearchFragment에 리사이클러뷰와 어댑터 적용하기.

 

package com.example.kakaobook.ui.view.ui.view

import android.os.Bundle
import android.text.Editable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kakaobook.databinding.FragmentSearchBinding
import com.example.kakaobook.ui.view.data.model.util.Constants.Companion.SEARCH_BOOKS_TIME_DELAY
import com.example.kakaobook.ui.view.ui.view.adapter.BookSearchAdapter
import com.example.kakaobook.ui.view.ui.view.viewmodel.BookSearchViewModel


class SearchFragment : Fragment() {
    private var mbinding: FragmentSearchBinding? = null
    private val binding get() = mbinding!!

    private lateinit var bookSearchViewModel: BookSearchViewModel
    private lateinit var bookSearchAdapter: BookSearchAdapter


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mbinding = FragmentSearchBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        bookSearchViewModel = (activity as MainActivity).bookSearchViewModel

        setupRecyclerView()
        searchBooks()

        bookSearchViewModel.searchResult.observe(viewLifecycleOwner) {
            val books = it.documents
            bookSearchAdapter.submitList(books)

        }

    }

    //리사이클러뷰 사용하기
    //프래그먼트는 context가 될수없다고한다. 그러기 때문에 액티비티를 이용하여 부모 액티비티의 context를 받아와야한다
    private fun setupRecyclerView() {
        bookSearchAdapter = BookSearchAdapter()
        binding.SearchResult.layoutManager = LinearLayoutManager(context)
        binding.SearchResult.adapter = bookSearchAdapter

    }

    private fun searchBooks() {

        binding.SearchEditText.addTextChangedListener {
            it?.let {
                val query = it.toString().trim()
                if (query.isNotEmpty()) {
                    bookSearchViewModel.searchBooks(query)
                }
            }
        }
    }


    override fun onDestroyView() {
        mbinding = null
        super.onDestroyView()
    }

}

 

  • searchBooks()함수에  addTextChangedListener를 클릭시 SearchEdiitText가 입력되거나 값이 바뀔때  뷰모델 안에 전달되어  searchbooks가 실행된것을 알수있다.

 

  • 실행화면