2021.06.15 - [안드로이드/AAC, MVVM] - [Android] Paging 3 라이브러리 사용해보기 - (1) 데이터 페이징하기
에서 이어지는 글입니다.
addLoadStateListener와 LoadStateAdapter 를 사용하여
데이터의 로딩 상태를 얻어 UI에 나타낼 수 있다.
LoadState 객체를 통해 상태를 나타내며, 세가지 상태가 존재한다.
<?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">
<EditText
android:id="@+id/editTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:ems="10"
android:inputType="number"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editTextView" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button">
</androidx.recyclerview.widget.RecyclerView>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<Button
android:id="@+id/retryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/errorText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="데이터 로드에 실패하였습니다."
android:textSize="24sp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/retryButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
tools:visibility="visible" />
<TextView
android:id="@+id/emptyText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="데이터가 없습니다."
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
tools:visibility="visible"/>
</androidx.constraintlayout.widget.ConstraintLayout>
로드 상태를 나타낼 수 있는 xml을 만들어놓고
// 로딩 상태 리스너
myAdapter.addLoadStateListener { combinedLoadStates ->
binding.apply {
// 로딩 중 일 때
progressBar.isVisible = combinedLoadStates.source.refresh is LoadState.Loading
// 로딩 중이지 않을 때 (활성 로드 작업이 없고 에러가 없음)
recyclerView.isVisible = combinedLoadStates.source.refresh is LoadState.NotLoading
// 로딩 에러 발생 시
retryButton.isVisible = combinedLoadStates.source.refresh is LoadState.Error
errorText.isVisible = combinedLoadStates.source.refresh is LoadState.Error
// 활성 로드 작업이 없고 에러가 없음 & 로드할 수 없음 & 개수 1 미만 (empty)
if(combinedLoadStates.source.refresh is LoadState.NotLoading
&& combinedLoadStates.append.endOfPaginationReached
&& myAdapter.itemCount < 1){
recyclerView.isVisible = false
emptyText.isVisible = true
}
else{
emptyText.isVisible = false
}
}
}
addLoadStateListener 로
상태에 따른 UI visible 여부를 선언할 수 있다.
// 다시 시도하기 버튼
binding.retryButton.setOnClickListener {
myAdapter.retry()
}
또한 retry() 를 사용하면 데이터를 다시 불러올 수 있다.
로딩중일때 프로그래스바가 뜨는것을 확인할 수 있고
로드 실패 ( 네트워크 미연결 등) 되었을 때 화면이 뜨는 것을 확인할 수 있으며
uiserId아이템이 10까지만 있기때문에
없는 데이터를 호출했을 때 데이터가 없다는 화면이 뜨는 것을 확인할 수 있다.
LoadStateAdapter 으로 header와 footer를 설정할 수 있다.
<?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:gravity="center_horizontal"
android:orientation="vertical"
android:padding="10dp">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/errorText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="데이터 로드 실패"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/retryButton" />
<Button
android:id="@+id/retryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="다시 시도하기"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressBar" />
</androidx.constraintlayout.widget.ConstraintLayout>
헤더와 푸터로 사용할 레이아웃을 만들어주고
class MyLoadStateAdapter(private val retry: () -> Unit)
: LoadStateAdapter<MyLoadStateAdapter.LoadStateViewHolder>() {
override fun onBindViewHolder(
holder: MyLoadStateAdapter.LoadStateViewHolder,
loadState: LoadState
) {
holder.bind(loadState)
}
override fun onCreateViewHolder(
parent: ViewGroup,
loadState: LoadState
): MyLoadStateAdapter.LoadStateViewHolder {
val binding = LoadStateBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return LoadStateViewHolder(binding)
}
inner class LoadStateViewHolder(private val binding: LoadStateBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
// 버튼 클릭 시 Fragment 에서 받아온 동작 호출 (retry)
binding.retryButton.setOnClickListener {
retry.invoke()
}
}
fun bind(loadState : LoadState){
binding.apply {
progressBar.isVisible = loadState is LoadState.Loading
retryButton.isVisible = loadState !is LoadState.Loading
errorText.isVisible = loadState !is LoadState.Loading
}
}
}
}
LoadStateAdapter 를 상속받은 클래스를 추가해준다.
여기서도 동일하게 LoadState 에 따른 UI를 지정해주면 된다.
파라미터는 람다식으로 함수를 작성하여
binding.apply {
recyclerView.setHasFixedSize(true) // 사이즈 고정
// header, footer 설정
recyclerView.adapter = myAdapter.withLoadStateHeaderAndFooter(
header = MyLoadStateAdapter { myAdapter.retry() },
footer = MyLoadStateAdapter { myAdapter.retry() }
)
}
사용하는 곳에서 retry() 함수를 받아온다.
LoadResult.Page(
data = post!!,
prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
nextKey = position + 1 // 계속 반복되게 해둠
)
페이지를 계속해서 불러오기 위해 nextKey를 변경하였다.
데이터를 다시 불러와야 할때 네트워크가 꺼져있다면
하단에서 잘 표시해주는 것을 볼 수 있고
maxsize를 20으로 해두었기 때문에
네트워크를 끄고 위로 스크롤하여 20개 이상의 데이터를 불러오면
상단에서 로드 실패를 잘 표시해주는 것을 볼 수 있다.
https://github.com/HanYeop/AndroidStudio-Practice2/tree/master/PagingEx