오늘은 Paging 라이브러리와 RecyclerView,
그리고 Room을 이용해 대량의 데이터를 구현하는 방법에 대해 알아보겠습니다.
1. Paging
1-1. Paging라이브러리
Android Paging Library는,
앱에서 대량의 데이터를 표시하는 데 도움을 주는 라이브러리입니다.
책을 페이지별로 나누어 보는 것처럼,
데이터를 작은 덩어리(페이지)로 나누어서,
필요한 부분만 불러와 보여주는 방식입니다.
Paging이 필요한 시기는 언제일까요?
처음 로드해야하는 데이터가, 50개 정도를 넘어선다면,
Paging을 고려해 보아야 합니다.
1-2. Paging 라이브러리를 사용해야 하는 이유
Paging라이브러리가,
데이터를 덩어리로 나누어주는 것만 하는 것이 아니고요.
아래와 같은 역할에 대한 코딩을 줄여주기도 합니다.
- 자동 메모리 관리: 화면에 보이지 않는 아이템들을 메모리에서 해제 후, 필요할 때 다시 로드
- 스크롤 최적화: 사용자가 스크롤하기 전에 미리 다음 페이지 로드해 부드러운 스크롤 경험 제공
- 상태 관리: 로딩 상태, 에러 상태, 데이터 새로고침 상태등을 관리해 줌
- RecyclerView와의 통합: DiffUtil을 자동으로 처리해서, 페이지 로딩 중에도 UI가 버벅이지 않도록 해 줌
1-3. Paging
심플하게 생각해보면,
Database-> ViewModel의 dao함수를 거쳐, UI에 전달되었던 데이터가,
Database-> ViewModel에서,
dao함수를 가진 Pager를 거쳐서 UI에 전달된다고 생각하면 쉽습니다.
Pager의 구현은 아래에서도 보겠지만,
아래와 같은 형식으로 구현됩니다.
flow타입으로 로드할 데이터베이스의 함수를 Pager가 중간에서,
보여줘야할 페이지만큼 로드해서 보내준다는 것 입니다.
val students = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 40
)
) {
database.studentDao().getAllStudents()
}.flow.cachedIn(viewModelScope)
2. Room 구현
그럼 이제 구현을 해보면서 Pager를 익혀 보겠습니다.
데이터베이스가 있어야, Pager가 중간에서 역할을 할 수 있지요.
먼저 Room을 구현하겠습니다.
Room에 관한 자세한 내용은 아래 글을 참조해 주세요.
>> Room DB 사용방법 총정리 # Android SQLite
2-1. Entity
먼저 아래와 같이 Student Entity를 구성해 줍니다.
@Entity(tableName = "students")
data class Student(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val score: Int,
val age: Int
)
2-2. Dao
Dao함수가 있어야,
페이지에게 필요한 데이터를 전달해 줄 수 있겠지요.
핵심은 getAllStudents()가 PagingSource타입으로 값을 리턴해준다는 것 입니다.
@Dao
interface StudentDao {
@Query("SELECT * FROM students ORDER BY name ASC")
fun getAllStudents(): PagingSource<Int, Student>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(students: List<Student>)
}
2-3. Database클래스
Room이 동작하게 하기위해,
Database클래스도 아래와 같이 동작시킵니다.
@Database(entities = [Student::class], version = 1)
abstract class StudentDatabase : RoomDatabase() {
abstract fun studentDao(): StudentDao
}
3. RecyclerView의 Adapter와 ViewHolder
이제는 RecyclerView를 구현할 차례입니다.
class StudentAdapter : PagingDataAdapter<Student, StudentAdapter.StudentViewHolder>(STUDENT_COMPARATOR) {
class StudentViewHolder(private val binding: ItemStudentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(student: Student?) {
student?.let {
binding.tvName.text = it.name
binding.tvScore.text = "Score: ${it.score}"
binding.tvAge.text = "Age: ${it.age}"
}
}
}
override fun onBindViewHolder(holder: StudentViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudentViewHolder {
return StudentViewHolder(
ItemStudentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
companion object {
private val STUDENT_COMPARATOR = object : DiffUtil.ItemCallback<Student>() {
override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean {
return oldItem == newItem
}
}
}
}
4. ViewModel에서 구현하기
ViewModel에서 아래와 같이 구현할 수 있습니다.
Pager의 인자에,
page당 몇개의 아이템들을 불러올지,
초기 로드할 사이즈는 몇개인지를 정해서 알려줍니다.
그리고 데이터를 로드할 때 사용할 dao함수를 넣어줍니다.
class StudentViewModel(private val database: StudentDatabase) : ViewModel() {
val students = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 40
)
) {
database.studentDao().getAllStudents()
}.flow.cachedIn(viewModelScope)
}
pager에 flow.cachedIn(viewModelScope)을 해 주었는데요.
이렇게 하면,
새로운 페이지가 로드될 때마다 새로운 PagingData를 flow타입으로 방출해 줍니다.
이렇게 flow타입으로 데이터가 흘러나오면,
Activity나 Fragment에서 collect만 해주면 쉽게,
데이터를 observe할 수 있게됩니다.
cachedIn은 PagingData를 공유하고 캐시해 주는데요.
다음과 같은 역할들을 해 줍니다.
- 이미 로드된 데이터를 메모리에 캐시해, 불필요한 재로딩 방지
- 여러 collector들 간에 동일한 페이징 데이터 공유
- 화면 회전시에도 데이터 유지
5. Activity에서 구독
위에서 flow로 흘러나오는 데이터를,
Activity에서 collectLatest를 이용해 구독해줍니다.
class MainActivity : AppCompatActivity() {
private val viewModel: StudentViewModel by viewModels()
private val adapter = StudentAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.adapter = adapter
lifecycleScope.launch {
viewModel.students.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
}
}
그런데 왜 collect가 아니라, collectLatest일까요?
아래 둘은 다음과 같은 차이가 존재합니다.
- collect: 이전데이터에 대한 collect블록내부의 처리가 완료될때까지, 다음 데이터를 처리를 기다림
- collectLatest: 새로운 데이터가 들어오면, 이전작업 취소하고 최신 데이터 처리
대량의 데이터를 다루는 pagingData에서,
유저가 빠르게 스크롤 할 수도 있는데요.
이 경우 둘은 다음과 같은 차이가 발생하게 됩니다.
- collect 사용 시: 사용자는 오래된 데이터를 모두 보게 됨
- - 페이지 1 완료까지 대기
- - 페이지 2 완료까지 대기
- - 페이지 3 처리
- collectLatest 사용 시: 사용자는 최신 데이터만 보게 됨
- 페이지 1 로딩 중 페이지 2 요청 → 페이지 1 취소
- 페이지 2 로딩 중 페이지 3 요청 → 페이지 2 취소
- 페이지 3 처리
6. 기존에 RecyclerView를 구현하고 있을 경우 마이그레이션
기존에 RecyclerView를 구현중이라면,
아래와 같은 순서로 마이그레이션 해주면 됩니다.
6-1. Dao수정하기
가장 먼저 할 것은 dao클래스를 수정해 주는 것 입니다.
아래와 같이 구성되어 있었을 텐데요.
@Dao
interface StudentDao {
@Query("SELECT * FROM students ORDER BY name ASC")
fun getAllStudents(): Flow<List<Student>>
}
리턴 타입을 아래와 같이,
PagingSource타입으로 해 줍니다.
@Dao
interface StudentDao {
@Query("SELECT * FROM students ORDER BY name ASC")
fun getAllStudents(): PagingSource<Int, Student>
}
6-2. Adapter 수정하기
원래 RecyclerView라면 Adapter를 아래와 같이 상속하고 있을 텐데요.
class StudentAdapter : RecyclerView.Adapter<StudentViewHolder>(){
...
}
아래와 같이 PagingDatatAdapter를 상속하도록 수정해 주고,
인자에 DiffUtil을 구현한 객체를 넘겨줍니다.
class StudentAdapter
: PagingDataAdapter<Student, StudentViewHolder>(STUDENT_COMPARATOR) {
companion object {
private val STUDENT_COMPARATOR = object : DiffUtil.ItemCallback<Student>() {
override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean {
return oldItem == newItem
}
}
}
}
6-3. ViewModel에서 Pager로 Wrapping
ViewModel에서는,
Pager로 dao함수를 wrapping해 줍니다.
class StudentViewModel(private val database: StudentDatabase) : ViewModel() {
val students = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false
)
) {
database.studentDao().getAllStudents()
}.flow.cachedIn(viewModelScope)
}
이상으로 대량의 데이터를,
Paging 라이브러리, RecyclerView 그리고 Room을 이용해서,
불러오는 방법에 대해 정리해 보았습니다.
'Android 개발 > RecyclerView, List' 카테고리의 다른 글
DiffUtil 이용해서 업데이트 하는 RecyclerView (0) | 2021.04.15 |
---|---|
RecyclerView에서 고차함수 이용한 리스트아이템클릭 구현 (0) | 2021.04.02 |
View가 크기변화를 감지하도록 하는 방법 (addOnLayoutChangeListener) (0) | 2017.07.26 |
Drag해서 RecyclerView (listview) 순서 바꾸기 (2) | 2017.07.20 |
notifyDataSetChanged 실행시에 깜박이는 현상 해결 (0) | 2017.07.02 |
Checkable interface 로 selectable한(선택할 수 있는) RecyclerView 리스트 만들기 (0) | 2017.01.20 |
RecyclerView 로 listview 정복 Part.2 #리스트 아이템 클릭 (6) | 2016.12.25 |
RecyclerView 로 listview 구현 방법 마스터하기 (6) | 2016.12.20 |
댓글