Android/Side Projects

도서 리뷰 앱 개발하기

화요밍 2022. 2. 11. 21:46
728x90
반응형
Side Project - Book Review App
인터파크 도서 Open API를 사용해서 베스트 셀러 목록을 보여주고 검색을 통해 책을 검색하는 도서 리뷰 앱 프로젝트

 

 

GitHub - hwayeon351/Book-Review-App

Contribute to hwayeon351/Book-Review-App development by creating an account on GitHub.

github.com


학습 회고

오늘은 베스트셀러 목록과 검색 결과를 받아와서 RecyclerView에 데이터를 바인딩하여 앱 화면에 띄우는 과정을 구현하였다.

 


오늘 공부한 내용

  • RecyclerView

RecyclerView는 여러 가지 항목을 나열하는 목록 화면을 만들 때 사용한다.

RecyclerView는 개별 요소들을 재활용하는데, 항목이 스크롤되어서 화면에서 벗어나더라도 RecyclerView는 뷰를 제거하지 않는다.

해당 뷰를 화면에서 스크롤되어 나타난 새 항목의 뷰로 재사용한다.

이러한 RecyclerView는 앱의 응답성을 개선하고 전력 소모를 줄이기 때문에 성능을 개선한다.

 

  • RecyclerView 구성 요소

1. ViewHolder: 각 항목을 구성하는 뷰 객체를 가진다. 목록에 있는 각 항목의 레이아웃을 포함하는 View의 래퍼이다.

2. Adapter: ViewHolder의 뷰 객체에 데이터를 바인딩하여 항목을 구성한다.

3. LayoutManager: Adapter가 만든 항목을 어떻게 배치할지를 결정하고 RecyclerView에 출력한다.

 

 

  • 항목의 레이아웃 구성하기

RecyclerView의 각 항목에 들어갈 레이아웃을 구성한다.

다음은 item_book.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">

    <ImageView
        android:id="@+id/coverImageView"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@drawable/background_gray_stroke_radius_16"
        android:padding="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/titleTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="12dp"
        android:ellipsize="end"
        android:lines="1"
        android:textColor="@color/black"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/coverImageView"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="안드로이드 마스터하기" />

    <TextView
        android:id="@+id/descriptionTextView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="12dp"
        android:ellipsize="end"
        android:maxLines="3"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@id/titleTextView"
        app:layout_constraintTop_toBottomOf="@id/titleTextView" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

  • ViewHolder 구현하기

ViewHolder는 RecyclerView.ViewHolder를 상속받아서 작성해야 한다. VIewHolder를 Adapter의 inner class로 구현하였다.

원래는 ViewHolder에 항목의 뷰 객체를 선언하고 findViewById로 가져와야 하는데, View Binding 기법을 이용해서 항목 레이아웃 xml 파일에 해당하는 바인딩 객체만 가지고 있으면 된다.

바인딩 객체에 항목을 구성하는 뷰들이 자동으로 선언되었기 때문에 간단한 코드로 사용할 수 있다.

 

나는 항목 레이아웃을 item_book.xml로 작성하였기 때문에 ItemBookBinding 객체를 가져왔다.

BookItemViewHolder에 bind라는 메서드를 생성해서 Adapter에서 함수를 호출해 데이터를 바인딩 할 수 있도록 하였다.

inner class BookItemViewHolder(private val binding: ItemBookBinding): RecyclerView.ViewHolder(binding.root) {
    fun bind(bookModel: Book) {
        binding.titleTextView.text = bookModel.title
        binding.descriptionTextView.text = bookModel.description

        Glide.with(binding.coverImageView.context)
            .load(bookModel.coverSmallUrl)
            .into(binding.coverImageView)
    }
}

 

 

  • Adapter 구현하기

Adapter는 ViewHolder의 뷰에 데이터를 바인딩해서 각 항목을 만들어 주는 역할을 한다.

RecyclerView.Adapter를 상속받아 작성하는 방법도 있지만, 뒤에서 설명할 DiffUtil을 활용하기 위해서 ListAdapter를 상속해서 어댑터를 구현하였다.

ListAdapter에 데이터 클래스를 담아서 Adapter 내에 데이터 리스트를 정의하지 않아도 된다. 또한, currentList로 현재 어댑터가 가지고 있는 리스트를 가져올 수 있다.

  1. onCreateViewHolder() - 항목의 뷰를 가지는 ViewHolder를 준비하기 위해 호출된다. onCreateViewHolder() 함수에서 반환한 ViewHolder 객체는 onBindViewHolder() 함수의 매개변수로 전달된다.
  2. onBindViewHolder() - ViewHolder의 뷰에 데이터를 바인딩하기 위해 호출된다.
class BookAdapter: ListAdapter<Book, BookAdapter.BookItemViewHolder>(diffUtil) {
	//ViewHolder
    inner class BookItemViewHolder(private val binding: ItemBookBinding): RecyclerView.ViewHolder(binding.root) {
        fun bind(bookModel: Book) {
            binding.titleTextView.text = bookModel.title
            binding.descriptionTextView.text = bookModel.description

            Glide.with(binding.coverImageView.context)
                .load(bookModel.coverSmallUrl)
                .into(binding.coverImageView)
        }
    }

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

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

 

 

  • DiffUtil

DiffUtil은 두 개의 리스트의 다른점을 계산하고 첫 번째 목록을 두 번째 목록으로 변환하는 업데이트 작업 목록을 출력하는 유틸리티 클래스이다.

DiffUtil을 활용해서 RecyclerView Adapter에 데이터를 업데이트하는데 사용할 수 있다.

ListAdapter는 DiffUtil을 백그라운드 스레드에서 간단하게 사용할 수 있도록 한다.

ListAdapter의 submitList() 함수를 호출해서 매개변수에 RecyclerView에 넣어야 할 리스트 데이터를 담아주면 DiffUtil이 동작하고 항목이 업데이트 된다.

 

RecyclerView에 항목으로 동적으로 추가하거나 제거할 때마다 notifyDataSetChanged()를 사용하였다.

이는 RecyclerView의 항목이 바뀌었으니 다시 처음부터 RecyclerView에 항목을 구성하는 방식으로 동작한다.

만약, 항목의 개수가 많다면 다시 처음부터 항목을 구성해야하기 때문에 지연 시간이 길어질 수 있고, RecyclerView에 Flickering Issue가 발생한다.

이를 보완하기 위해 나온 방법이 DiffUtill이다. DiffUtil을 사용하면 현재 리스트와 교체되어야 하는 리스트를 비교해서 바뀌어야 하는 데이터만 바꿔주기 때문에 효율적이다.

 

BookAdapter가 생성될 때 함께 생성되는 companion object로 DiffUtil.ItemCallback을 구현해주었다.

  1. areItemsTheSame() - oldItem과 newItem이 같은 항목인지 확인한다.
  2. areContentsTheSame() - oldItem과 newItem이 같은 데이터인지 확인한다. areItemsTheSame()에서 true가 나오면 추가적으로 비교하기 위해 사용된다.
companion object {
    val diffUtil = object: DiffUtil.ItemCallback<Book>() {
        override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean {
            return oldItem == newItem
        }

        override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean {
            return oldItem.id == newItem.id
        }

    }
}

 

  • 리싸이클러뷰에 레이아웃 매니저와 어댑터 등록하기
private fun initBookRecyclerView() {
    adapter = BookAdapter()
    binding.bookRecyclerView.layoutManager = LinearLayoutManager(this)
    binding.bookRecyclerView.adapter = adapter
}

 

  • View Binding 기법

뷰 바인딩 기능을 사용하면 뷰와 상호작용하는 코드를 쉽게 작성할 수 있다.

모듈에서 설정된 뷰 바인딩 기능은 모듈에 있는 각 XML 레이아웃 파일의 바인딩 클래스를 생성한다.

바인딩 클래스의 인스턴스를 통해 레이아웃의 ID가 있는 모든 뷰에 관해 직접 참조가 가능하다.

즉, 뷰 바인딩 기능을 사용하면 findViewById를 대체할 수 있다.

 

  • findViewById를 사용했을 때보다 좋은 점
  1. Null Safety - 뷰 바인딩은 뷰의 직접 참조를 생성하기 때문에 유효하지 않은 뷰 ID로 인해 null pointer exception이 발생할 위험이 없다.
  2. Type Safety - xml 파일에 참조한 뷰들을 매칭해서 바인딩 클래스에서 각각의 필드에 대한 타입을 가지고 있기 때문에 cast exception이 발생할 위험이 없다.

 

뷰 바인딩 기능을 사용하려면 module의 gradle에 뷰 바인딩을 true로 해주어야 한다.

viewBinding {
    enabled = true
}​

 

 

이후, 뷰 바인딩을 사용하는 방법은 레이아웃 파일 이름이 activity_main.xml이라면, 생성된 바인딩 클래스 이름은 ActivityMainBinding이다. 바인딩 클래스에 inflate() 메서드를 호출하면 바인딩 클래스의 인스턴스가 생성된다.

루트 뷰를 setContentView()에 전달해서 화면상에서 활성화된 뷰로 만들면 된다.

binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.레이아웃에지정한id를 통해 뷰를 참조할 수 있으며, binding.root는 레이아웃의 루트 뷰를 반환한다.

 

https://developer.android.com/topic/libraries/view-binding

 

뷰 결합  |  Android 개발자  |  Android Developers

뷰 결합 뷰 결합 기능을 사용하면 뷰와 상호작용하는 코드를 쉽게 작성할 수 있습니다. 모듈에서 사용 설정된 뷰 결합은 모듈에 있는 각 XML 레이아웃 파일의 결합 클래스를 생성합니다. 바인딩

developer.android.com

 


참고

  • Do it 깡샘의 안드로이드 앱 프로그래밍 with 코틀린 - 저자 강성윤

 

728x90
반응형

'Android > Side Projects' 카테고리의 다른 글

도서 리뷰 앱 개발하기  (0) 2022.02.13
도서 리뷰 앱 개발하기  (0) 2022.02.12
도서 리뷰 앱 개발하기  (1) 2022.02.10
My Alarm 앱 개발하기  (0) 2022.02.09
My Alarm 앱 개발하기  (0) 2022.02.08