책 검색 앱 만들기(4) - 검색결과를 UI에 표시하기
- 이미지를 보여주기위해 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가 실행된것을 알수있다.
- 실행화면