일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- github api
- RETROFIT
- AndroidX
- 백준 15686
- ViewModel
- 셀레니움
- 안드로이드 플랫폼
- Android 컴파일
- 수리통계
- Devexpress
- Clean Architecture
- Kotiln
- gson
- python3
- okHttp
- REST API
- Java
- 필답고사
- TODO
- 웹 크롤링
- TabLayout
- RecyclerView
- LRU
- FragmentStateAdapter
- Android
- 안드로이드 API
- 데이터바인딩
- kotlin
- 통계대학원
- ViewPager2
- Today
- Total
그냥 가끔의 기록장
[Kotlin] Github API를 이용해 예제 App 만들기 - 2-1. RecyclerView로 데이터 보여주기 본문
[Kotlin] Github API를 이용해 예제 App 만들기 - 2-1. RecyclerView로 데이터 보여주기
the phoenix 2022. 8. 4. 23:16Github Open API를 이용해 예제 앱 만들기 두번째 글이다.
1. Retrofit 이용해 Github API 연동, Gson으로 변환 후 Log로 출력해 보기: https://soeun-87.tistory.com/36
2. RecyclerView + ListAdapter 구현하여 1단계의 정보들 UI로 표현하기 <-- 현재 글
3. Android Jetpack Room + Diff Util 같이 적용해보기
4. Coroutine 정복하기
5. MVVM 적용하기 - Android Jetpack ViewModel, LifeCycle, LiveData 같이 알아보기
6. DI란? Android Jetpack Hilt 적용기
7. apk가 뭐지.. Android Studio에서 apk 파일 추출해보기
+ 8. 가능하면 Unit Test 도입
분량 상, Recyclerview와 ListAdapter는 두 번에 걸쳐 작성하려 한다. 이번 글은 Recyclerview 도입만 작성한다.
Recyclerview
1. 개요
(1) Recyclerview란?
우선, 오늘 공부할 RecyclerView는 안드로이드 Jetpack인 androidx에 포함되어 있는 RecyclerView이다. (v7이나 다른 RecyclerView는 너무 예전 버전이니 androidx의 RecyclerView를 사용하자) RecyclerView는 구글 공식 문서에 따르면, 메모리 사용량을 최소화하면서 UI에 많은 양의 데이터를 표시해주는 view라고 한다.
https://developer.android.com/jetpack/androidx/releases/recyclerview
보다 자세히 설명하면, RecyclerView는 수많은 데이터 집합을 각각의 아이템 단위로 구성해 화면에 출력하는 viewGroup이며, 데이터량이 너무 많아 한 화면에 한번에 표시하기 힘들기에, 스크롤을 가능하게 한 아이템 리스트뷰다.
즉, RecyclerView는 기존의 Listview의 성능을 확장시킨 것이라 이해하면 된다!
여기서 하나 궁금한 점이 생긴다. RecyclerView는 이름에 왜 Recycle이 들어갈까? 뭘 재활용한다는 걸까?
(2) 왜 Recycle이 들어갈까?
앞서 RecyclerView를 Listview와 비교했었는데, Listview는 리스트의 항목이 갱신될 때마다 매번 아이템 뷰를 새로 그린다는 가장 큰 문제점이 있었다. (스크롤을 할 때마다 위에 있던 뷰는 삭제되고, 밑에 있는 뷰는 계속 생겨 cost가 매우 높아진다.) 이렇게 매번 뷰를 새로 그리는 것은 수 많은 데이터를 표시하는데에 성능 저하를 일으킬 수 있다.
RecyclerView는 이러한 Listview의 단점을 개선한 것으로, RecyclerView는 아이템을 표시하기 위해 만든 뷰를 "재활용" 한다. 화면에 보이는 몇 개의 뷰만 생성하고, 스크롤을 하면 앞서 보였던 뷰를 삭제하지 않고 가장 아래로 이동시켜 재활용한다. 이를 위해 ViewHolder 패턴을 사용하여 맨 첫 화면에 보이는 아이템 view 객체를 기억(holding)한다.
또한 Listview는 아이템들을 수직 방향으로만 나열할 수 있었던 것에 비해, RecyclerView는 수직, 수평 방향을 모두 지원하며 아이템 뷰를 동적으로 구성할 수도 있게 해준다.
2. 구성요소
- RecyclerView
- androidx에서 제공되는 위젯으로, 앞서 소개한 내용처럼 사용자의 데이터를 리스트 형태로 화면에 표시하며 스크롤을 할 때마다 아이템 뷰를 재활용해 보여주는 컨테이너이다.
- Adapter
- 데이터 테이블을 아이템 단위의 뷰 목록으로 보여주기 위해 데이터와 RecyclerView 사이의 다리 역할을 하는 객체 (Listview에서도 구현해야 함)
- 즉, data --> adapter --> RecyclerView
- Layout Manager
- RecyclerView는 아이템 뷰들을 수직 방향 뿐 아니라 수평, 격자 등 다양한 형태의 레이아웃으로 나타낼 수 있다. 이렇게 다양한 형태의 레이아웃을 제공하기 위해 RecyclerView는 Layout Manager를 사용한다. (Adapter에서 아이템 뷰를 생성하기 전에 Layout Manager가 아이템 뷰를 어떻게 배치할 지 형태를 결정함)
- ViewHolder
- 화면에 표시될 아이템 뷰를 "저장"하는 객체
- 미리 생성된 ViewHolder 객체가 있는 경우는 새로 뷰를 생성하지 않고 이미 만들어져 있는 ViewHolder를 재활용한다. 이 경우 데이터가 ViewHolder의 아이템 뷰에 바인딩된다.
그림을 통해 다시보면, 데이터 목록들이 있을 때, 이를 화면에 각각의 아이템 뷰 & 스크롤을 함께 제공하는 것이 RecyclerView이다. 이렇게 각 데이터를 아이템 뷰로 매핑해주기 위해 데이터와 RecyclerView의 중간에서 다리 역할을 하는게 Adapter이다. 단, RecyclerView는 다양한 형태의 레이아웃을 제공하기 위해 Adapter에서 아이템 뷰를 생성하기 전에 뷰 그룹을 어떻게 배치할지 Layout Manager로 결정한다.
3. 구현
1. viewBinding, Picasso gradle에 추가
- viewBinding은 필수가 아니나, findViewById와 같은 보일러 플레이트 코드를 줄이고자 viewBinding을 사용한다. 이를 위해서 Module 수준의 build.gradle에 다음과 같이 추가한다.
- 본 프로젝트는 url을 통해 user의 thumbnail을 보여주는 부분이 포함되어 있으므로, 외부로부터 이미지를 불러와야 할 경우 유용하게 쓸 수 있는 라이브러리인 Picasso를 사용한다. (비슷한 기능으로 Glide 라이브러리를 사용할 수도 있다.) 다른 라이브러리를 import 할 때와 마찬가지로 gradle에 dependency를 추가해준다.
buildFeatures {
viewBinding true
dataBinding true
}
dependencies {
implementation 'com.squareup.picasso:picasso:2.8'
}
2. 아이템 뷰 xml 작성
- RecyclerView에서 각 데이터를 보여줄 아이템 뷰 xml을 작성한다. 데이터가 리스트라 가정할 때, 1 데이터 아이템 - 1 아이템뷰로 매핑된다고 보면 된다.
- thumbnail은 ImageView로, name과 url은 TextView로 보여준다.
<?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">
<ImageView
android:id="@+id/ivThumbnail"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvName"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginStart="20dp"
app:layout_constraintStart_toEndOf="@+id/ivThumbnail"
app:layout_constraintTop_toTopOf="@+id/ivThumbnail" />
<TextView
android:id="@+id/tvUrl"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginTop="10dp"
android:layout_marginStart="80dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvName" />
</androidx.constraintlayout.widget.ConstraintLayout>
3. Activity xml에 RecyclerView 추가
- RecyclerView를 사용하기 위해 Activity의 xml에 추가한다. 그냥 제일 간단한 예시를 하기 위해 다른 view는 추가하지 않았다.
<?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=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvUsers"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
4. Model 생성
- 앞서 RecyclerView는 1 데이터 아이템 - 1 아이템 뷰가 매핑된다고 했다. 본 프로젝트에서는 데이터를 Github api를 통해 가져오는데, User data class는 1번 글에서 사용했던 대로 작성했다. (https://soeun-87.tistory.com/36 의 dto 참고)
package com.example.usinggithubapisampleapp
import com.google.gson.annotations.SerializedName
data class User(
@SerializedName("bio")
val bio: String,
@SerializedName("url")
val userUrl: String,
@SerializedName("login")
val userName: String,
@SerializedName("avatar_url")
val thumbnail: String
)
5. Adapter 생성
- 이번 글에서 가장 중요한 부분이다. UserAdapter 클래스를 만들어주는데, RecyclerView.Adapter<UserAdapter.UserViewHolder>()를 상속시켜준다. 여기서 UserAdapter.UserViewHolder는 Adapter가 사용할 ViewHolder인데, ViewHolder를 별도의 파일로 뺄 수도 있으나 여기선 inner class로 작성하였다. UserViewHolder는 bind 함수를 포함하는데, 해당 함수는 데이터를 인자로 받아서 각 뷰에 데이터를 매핑해 bind하는 역할을 한다.
- RecyclerView.Adapter를 상속 시 onCreateViewHolder, onBindViewHolder, getItemCount라는 세 함수를 무조건 override해야 한다.
- onCreateViewHolder: ViewHolder에 layout inflate하는 함수로, ViewHolder 생성
- onBindViewHolder: ViewHolder 재활용하여 데이터를 뷰에 bind 하는 함수 (UserViewHolder의 bind 함수를 사용함)
- getItemCount: RecyclerView에서 표시해줄 데이터 개수 (보통 인자로 받는 userList 등의 size를 반환)
package com.example.usinggithubapisampleapp
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.usinggithubapisampleapp.databinding.ItemUserBinding
import com.squareup.picasso.Picasso
class UserAdapter(var userList: List<User>): RecyclerView.Adapter<UserAdapter.UserViewHolder>(){
private lateinit var itemBinding: ItemUserBinding
inner class UserViewHolder(private val itemBinding: ItemUserBinding): RecyclerView.ViewHolder(itemBinding.root){
fun bind(data: User){
if (data.thumbnail != "") {
Picasso.get()
.load(data.thumbnail)
.centerCrop()
.fit()
.into(itemBinding.ivThumbnail)
}
itemBinding.tvName.text = data.userName
itemBinding.tvUrl.text = data.userUrl
}
}
// RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - viewHolder에 layout inflate 하는 함수 (ViewBinding 사용)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
itemBinding = ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return UserViewHolder(itemBinding)
}
// RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - viewHolder에 각 view를 bind하는 함수
// UserViewHolder내에 bind함수 정의했으므로, 각 userList[position]인 item data랑 view를 bind하면 됨
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(userList[position])
}
// RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - 보통 Todos.size를 return, RecyclerView내의 item 개수 return하는 함수
override fun getItemCount(): Int {
return userList.size
}
}
6. Activity.kt의 RecyclerView에 Adapter와 LayoutManager 설정
- Activity의 RecyclerView에 Adapter를 설정
- 데이터를 Retrofit을 통해 받아오므로, getUserList 함수에서 response.body를 성공적으로 받은 경우에 adapter를 설정하는 setAdapter 함수를 호출한다.
- setAdapter
- adapter 생성
- onBindViewHolder 최적화
- RecyclerView에 adapter 등록
- adapter에 LayoutManager 등록
package com.example.usinggithubapisampleapp
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.usinggithubapisampleapp.databinding.ActivityMainBinding
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var adapter: UserAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. View Binding 설정
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 2. retrofit으로 userlist 불러온 후, Adapter 생성 및 설정
getUserList(20)
}
private fun setAdapter(userList: List<User>){
adapter = UserAdapter(userList)
// 1. onBindViewHolder 최적화
adapter.setHasStableIds(true)
// 2. adapter 설정
binding.rvUsers.adapter = adapter
// 3. RecyclerView에 LayoutManager 설정하기
binding.rvUsers.layoutManager = LinearLayoutManager(this)
}
// 특정 user 데이터를 갖고오는 함수
private fun getUser(userName: String){
RetrofitService.userAPI.getUser(username = userName).enqueue(
object : Callback<User> {
override fun onResponse(call: Call<User>, response: Response<User>) {
if (response.isSuccessful) {
if (response.code() == 200) {
Log.d("GET_USER", response.body().toString())
}
} else { // response.code == 400 or 300
Log.d("CLIENT_ERR", response.errorBody().toString())
}
}
override fun onFailure(call: Call<User>, t: Throwable) {
Log.d("RETROFIT_ERR", t.message.toString())
}
}
)
}
// userList 데이터를 갖고오는 함수
private fun getUserList(perPage: Int){
RetrofitService.userAPI.getUserList(per_page = perPage).enqueue(
object : Callback<List<User>> {
override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
if (response.isSuccessful) {
if (response.code() == 200) {
val body= response.body()
Log.d("SUCCESS", body.toString())
body?.let{
setAdapter(it)
}
}
} else { // response.code == 400 or 300
Log.d("CLIENT_ERR", response.errorBody().toString())
}
}
override fun onFailure(call: Call<List<User>>, t: Throwable) {
Log.d("RETROFIT_ERR", t.message.toString())
}
}
)
}
}
7. 결과
다음 글에서는 RecyclerView.Adapter가 아닌 ListAdapter를 사용하는 방법에 대해 작성할 예정이다!
Reference
https://velog.io/@hoyaho/RecyclerView
https://recipes4dev.tistory.com/154
'Android' 카테고리의 다른 글
[Kotlin] Android 톺아보기 1-2. Android 아키텍처 (0) | 2022.09.06 |
---|---|
[Kotlin] Android 톺아보기 1-1. Android 개요 (0) | 2022.09.03 |
[Kotlin] Github API를 이용해 예제 App 만들기 - 1. Retrofit 정복 (0) | 2022.08.01 |
[Kotlin] Todo 토이 프로젝트 [3단계] (ViewPager2 + RecyclerView + ViewModel) (1) | 2022.04.30 |
[Kotlin] Todo 토이 프로젝트 [2단계] (ViewPager2 + RecyclerView + ViewModel) (0) | 2022.04.23 |