Paging 2 라이브러리 개요 Android Jetpack의 구성요소

Paging 라이브러리를 사용하면 작은 데이터 청크를 한 번에 로드하여 표시할 수 있습니다. 요청에 따라 일부 데이터를 로드하면 네트워크 대역폭 및 시스템 리소스 사용량을 줄일 수 있습니다.

이 가이드에서는 라이브러리의 몇 가지 개념적인 예를 제공하며 라이브러리의 작동 방식을 개략적으로 설명합니다. 라이브러리 작동 방식의 전체 예를 확인하려면 추가 리소스 섹션의 Codelab 및 샘플을 사용해 보세요.

설정

페이징 구성요소를 Android 앱으로 가져오려면 앱의 build.gradle 파일에 다음 종속 항목을 추가하세요.

Groovy

dependencies {
  def paging_version = "2.1.2"

  implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}

Kotlin

dependencies {
  val paging_version = "2.1.2"

  implementation("androidx.paging:paging-runtime:$paging_version") // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation("androidx.paging:paging-common:$paging_version") // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation("androidx.paging:paging-rxjava2:$paging_version") // For Kotlin use paging-rxjava2-ktx
}

라이브러리 아키텍처

이 섹션에서는 페이징 라이브러리의 주요 구성요소를 설명하고 보여줍니다.

PagedList

페이징 라이브러리의 핵심 구성요소는 앱의 데이터 청크 또는 페이지를 로드하는 PagedList 클래스입니다. 더 많은 데이터가 필요하면 기존 PagedList 객체로 페이징됩니다. 로드된 데이터가 변경되면 PagedList의 새로운 인스턴스가 LiveData 또는 RxJava2 기반 객체에서 식별 가능한 데이터 홀더로 내보내집니다. PagedList 객체가 생성되면 UI 컨트롤러의 수명 주기가 준수되는 동안에는 앱의 UI에 콘텐츠가 표시됩니다.

다음 코드 스니펫은 PagedList 객체의 LiveData 홀더를 사용하여 데이터를 로드하고 표시하도록 앱의 뷰 모델을 구성하는 방법을 보여줍니다.

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

Java

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    // Creates a PagedList object with 50 items per page.
    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
                concertDao.concertsByDate(), 50).build();
    }
}

데이터

PagedList의 각 인스턴스는 그에 대응하는 DataSource 객체에서 앱 데이터의 최신 스냅샷을 로드합니다. 데이터는 앱의 백엔드 또는 데이터베이스에서 PagedList 객체로 흐릅니다.

다음 예는 Room 지속성 라이브러리를 사용하여 앱의 데이터를 구성하지만 다른 방법을 사용하여 데이터를 저장하려면 자체 데이터 소스 팩토리를 제공해도 됩니다.

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

Java

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a
    // PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

PagedList 객체에 데이터를 로드하는 방법을 자세히 알아보려면 페이징된 데이터 로드 방법에 관한 가이드를 참고하세요.

UI

PagedList 클래스는 PagedListAdapter와 함께 작동하여 항목을 RecyclerView에 로드합니다. 이러한 클래스는 함께 작동하여 콘텐츠 로드 시 콘텐츠를 가져와서 표시하며 보이지 않는 콘텐츠를 미리 가져오고 콘텐츠 변경사항을 애니메이션 처리합니다.

자세히 알아보려면 페이징된 목록 표시 방법에 관한 가이드를 참고하세요.

다양한 데이터 아키텍처 지원

페이징 라이브러리는 다음과 같은 데이터 아키텍처를 지원합니다.

  • 백엔드 서버에서만 제공된 데이터 아키텍처
  • 기기 내 데이터베이스에만 저장된 데이터 아키텍처
  • 기기 내 데이터베이스를 캐시로 사용하는 다른 소스의 조합

그림 1은 이러한 각 아키텍처 시나리오의 데이터 흐름 방식을 보여줍니다. 네트워크 전용 또는 데이터베이스 전용 솔루션의 경우 데이터가 앱의 UI 모델로 직접 흐릅니다. 조합된 접근 방식을 사용하고 있다면 데이터가 백엔드 서버에서 기기 내 데이터베이스로 흐른 후 앱의 UI 모델로 흐릅니다. 가끔은 각 데이터 흐름의 엔드포인트에서 로드할 데이터가 부족할 때도 있습니다. 그 시점에 엔드포인트는 데이터를 제공한 구성요소에 추가 데이터를 요청합니다. 예를 들어 기기 내 데이터베이스에서 데이터가 부족하면 데이터베이스는 서버에 추가 데이터를 요청합니다.

데이터 흐름 다이어그램
그림 1. Paging 라이브러리가 지원하는 각 아키텍처를 통해 데이터가 흐르는 방식

이 섹션의 나머지 부분에서는 각 데이터 흐름 구성의 추천 사용 사례를 제공합니다.

네트워크 전용

백엔드 서버의 데이터를 표시하려면 동기 버전의 Retrofit API를 사용하여 자체 맞춤형 DataSource 객체에 정보를 로드합니다.

데이터베이스 전용

RecyclerView를 설정하여 로컬 저장소를 관찰합니다. 이때 가급적이면 Room 지속성 라이브러리를 사용합니다. 그렇게 하면 데이터가 앱의 데이터베이스에 삽입되거나 수정될 때마다 관련 데이터를 표시하는 RecyclerView에 변경사항이 자동으로 반영됩니다.

네트워크 및 데이터베이스

데이터베이스 관찰을 시작했으면 PagedList.BoundaryCallback을 사용하여 데이터베이스에서 데이터가 부족한 시점을 인식할 수 있습니다. 그러면 네트워크에서 추가 항목을 가져와 데이터베이스에 삽입할 수 있습니다. UI가 데이터베이스를 관찰하고 있다면 이렇게만 하면 됩니다.

네트워크 오류 처리

페이징 라이브러리를 사용하여 표시되는 데이터를 가져오거나 페이징하는 데 네트워크를 사용할 때 다음과 같이 많은 연결이 끊기거나 취약해지기도 하므로 네트워크를 항상 '사용 가능' 또는 '사용 불가'로 취급하지 않는 것이 중요합니다.

  • 특정 서버가 네트워크 요청에 응답하지 못할 수 있습니다.
  • 기기가 느리거나 취약한 네트워크에 연결되었을 수 있습니다.

네트워크를 사용할 수 없는 상황에서는 앱에서 각 요청이 실패했는지 확인하고 최대한 정상적으로 복구해야 합니다. 예를 들어 데이터 새로고침 단계가 작동하지 않으면 사용자가 선택할 수 있는 '다시 시도' 버튼을 제공할 수 있습니다. 데이터 페이징 단계에서 오류가 발생하면 페이징 요청을 자동으로 다시 시도하는 것이 가장 좋습니다.

기존 앱 업데이트

앱이 이미 데이터베이스 또는 백엔드 소스의 데이터를 사용하고 있다면 페이징 라이브러리에서 제공하는 기능으로 직접 업그레이드할 수 있습니다. 이 섹션에서는 일반적인 기존 디자인이 있는 앱을 업그레이드하는 방법을 보여줍니다.

맞춤 페이징 솔루션

앱의 데이터 소스에서 소규모 데이터 하위 집합을 로드하는 데 맞춤 기능을 사용하면 로직을 PagedList 클래스의 로직으로 바꿀 수 있습니다. PagedList의 인스턴스는 기본적으로 일반 데이터 소스로의 연결을 제공합니다. 또한 이러한 인스턴스는 앱의 UI에 포함할 수 있는 RecyclerView 객체용 어댑터도 제공합니다.

페이지 대신 목록을 사용하여 로드된 데이터

메모리 내 목록을 UI 어댑터의 백업 데이터 구조로 사용할 경우 목록의 항목 수가 늘어날 가능성이 있다면 PagedList 클래스를 사용하여 데이터 업데이트를 관찰하는 것이 좋습니다. PagedList의 인스턴스는 LiveData<PagedList> 또는 Observable<List>를 사용하여 앱의 UI에 데이터 업데이트를 전달하기 때문에 로드 시간과 메모리 사용량을 최소화할 수 있습니다. 앱에서 List 객체를 PagedList 객체로 바꿀 때 앱의 UI 구조 또는 데이터 업데이트 로직을 변경할 필요가 없다는 점도 좋습니다.

CursorAdapter를 사용하여 데이터 커서를 목록 보기와 연결

앱에서 CursorAdapter를 사용하여 Cursor의 데이터를 ListView와 연결할 수 있습니다. 이 경우에는 대개 데이터를 ListView에서 앱의 목록 UI 컨테이너인 RecyclerView로 이전한 후, Cursor의 인스턴스가 SQLite 데이터베이스에 액세스하는지에 따라 Cursor 구성요소를 Room 또는 PositionalDataSource로 바꿔야 합니다.

Spinner의 인스턴스를 사용하는 경우 등 일부 상황에서는 어댑터 자체만 제공합니다. 그러면 라이브러리가 어댑터에 로드된 데이터를 가져와 표시합니다. 이러한 경우 어댑터의 데이터 유형을 LiveData<PagedList>로 변경한 다음, 라이브러리 클래스를 통해 UI의 항목을 확장하기 전에 ArrayAdapter 객체에 이 목록을 래핑합니다.

AsyncListUtil을 사용하여 콘텐츠를 비동기적으로 로드

AsyncListUtil 객체를 사용하여 정보 그룹을 비동기적으로 로드 및 표시하고 있다면 다음과 같이 페이징 라이브러리를 통해 데이터를 더 쉽게 로드할 수 있습니다.

  • 데이터가 특정 위치에 있지 않아도 됩니다. 페이징 라이브러리를 사용하면 네트워크에서 제공하는 키를 사용해 백엔드에서 직접 데이터를 로드할 수 있습니다.
  • 데이터가 셀 수 없을 정도로 많아도 됩니다. 페이징 라이브러리를 사용하면 남은 데이터가 없어질 때까지 데이터를 페이지에 로드할 수 있습니다.
  • 데이터를 더 쉽게 관찰할 수 있습니다. 페이징 라이브러리는 앱의 ViewModel이 보유하는 데이터를 식별 가능한 데이터 구조로 표시할 수 있습니다.

데이터베이스 예

다음 코드 스니펫은 모든 요소가 함께 작동되도록 하는 몇 가지 가능한 방법을 보여줍니다.

LiveData를 사용하여 페이징된 데이터 관찰

다음 코드 스니펫은 모든 요소가 함께 작동하는 것을 보여줍니다. concert 이벤트가 데이터베이스에서 추가, 삭제 또는 변경될 때 RecyclerView의 콘텐츠는 효율적으로 자동 업데이트됩니다.

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

class ConcertActivity : AppCompatActivity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: ConcertViewModel by viewModels()
        val recyclerView = findViewById(R.id.concert_list)
        val adapter = ConcertAdapter()
        viewModel.concertList.observe(this, PagedList(adapter::submitList))
        recyclerView.setAdapter(adapter)
    }
}

class ConcertAdapter() :
        PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
    fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
        val concert: Concert? = getItem(position)

        // Note that "concert" is a placeholder if it's null.
        holder.bindTo(concert)
    }

    companion object {
        private val DIFF_CALLBACK = object :
                DiffUtil.ItemCallback<Concert>() {
            // Concert details may have changed if reloaded from the database,
            // but ID is fixed.
            override fun areItemsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert.id == newConcert.id

            override fun areContentsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert == newConcert
        }
    }
}

Java

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
            concertDao.concertsByDate(), /* page size */ 50).build();
    }
}

public class ConcertActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ConcertViewModel viewModel =
                new ViewModelProvider(this).get(ConcertViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.concert_list);
        ConcertAdapter adapter = new ConcertAdapter();
        viewModel.concertList.observe(this, adapter::submitList);
        recyclerView.setAdapter(adapter);
    }
}

public class ConcertAdapter
        extends PagedListAdapter<Concert, ConcertViewHolder> {
    protected ConcertAdapter() {
        super(DIFF_CALLBACK);
    }

    @Override
    public void onBindViewHolder(@NonNull ConcertViewHolder holder,
            int position) {
        Concert concert = getItem(position);
        if (concert != null) {
            holder.bindTo(concert);
        } else {
            // Null defines a placeholder item - PagedListAdapter automatically
            // invalidates this row when the actual object is loaded from the
            // database.
            holder.clear();
        }
    }

    private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Concert>() {
        // Concert details may have changed if reloaded from the database,
        // but ID is fixed.
        @Override
        public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
            return oldConcert.getId() == newConcert.getId();
        }

        @Override
        public boolean areContentsTheSame(Concert oldConcert,
                Concert newConcert) {
            return oldConcert.equals(newConcert);
        }
    };
}

RxJava2를 사용하여 페이징된 데이터 관찰

LiveData가 아닌 RxJava2를 사용하고자 한다면 대신 Observable 또는 Flowable 객체를 만들 수 있습니다.

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: Observable<PagedList<Concert>> =
            concertDao.concertsByDate().toObservable(pageSize = 50)
}

Java

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final Observable<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;

        concertList = new RxPagedListBuilder<>(
                concertDao.concertsByDate(), /* page size */ 50)
                        .buildObservable();
    }
}

아래 스니펫에 있는 코드를 사용하여 데이터 관찰을 시작하고 중지할 수 있습니다.

Kotlin

class ConcertActivity : AppCompatActivity() {
    private val adapter = ConcertAdapter()

    // Use the 'by viewModels()' Kotlin property delegate
    // from the activity-ktx artifact
    private val viewModel: ConcertViewModel by viewModels()

    private val disposable = CompositeDisposable()

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val recyclerView = findViewById(R.id.concert_list)
        recyclerView.setAdapter(adapter)
    }

    override fun onStart() {
        super.onStart()
        disposable.add(viewModel.concertList
                .subscribe(adapter::submitList)))
    }

    override fun onStop() {
        super.onStop()
        disposable.clear()
    }
}

Java

public class ConcertActivity extends AppCompatActivity {
    private ConcertAdapter adapter = new ConcertAdapter();
    private ConcertViewModel viewModel;

    private CompositeDisposable disposable = new CompositeDisposable();

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        RecyclerView recyclerView = findViewById(R.id.concert_list);

        viewModel = new ViewModelProvider(this).get(ConcertViewModel.class);
        recyclerView.setAdapter(adapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        disposable.add(viewModel.concertList
                .subscribe(adapter.submitList(flowableList)
        ));
    }

    @Override
    protected void onStop() {
        super.onStop();
        disposable.clear();
    }
}

ConcertDaoConcertAdapter 코드는 LiveData 기반 솔루션용 코드이기 때문에 RxJava2 기반 솔루션용 코드와 동일합니다.

의견 보내기

다음 리소스를 통해 의견을 보내고 아이디어를 공유해 주세요.

Issue Tracker
버그를 수정할 수 있도록 문제를 신고해 주세요.

추가 리소스

Paging 라이브러리에 관해 자세히 알아보려면 다음 리소스를 참고하세요.

샘플

Codelab

동영상