개발 일지/기타

Pagination 인터페이스ㆍ구현ㆍ검증용 앱

interfacer_han 2025. 4. 2. 14:47

#1 Pagination 클래스

Pagination 클래스는 전체 게시글 갯수, 한 페이지에 표시할 게시글 갯수, 한 페이지 블럭에 표시할 페이지들의 갯수에 기반해 게시판의 네비게이션 역할을 하는 클래스다. 예전에 웹 프로그래밍에서 게시판을 만들 때 처음 만들었다. 그 이후로도 2 ~ 3번 정도 다시 Pagination 클래스를 재작성했던 기억이 있다. 얼마나 비효율적인가! 그래서 이참에 완벽하게 구현하고 기록해두려 한다. 앞으로 Pagination 클래스는 본 게시글의 코드를 재사용할 것이다.

 

#2 기본 코드

#2-1 인터페이스

// by interfacer_han (https://kenel.tistory.com/327)

/*
(example) goToFirstPageBlockButton: <<
(example) goToPreviousPageBlockButton: <
(example) goToNextPageBlockButton: >
(example) goToLastPageBlockButton: >>

(example) pageBlock 01: << < 01 02 03 04 05 06 07 08 09 10 > >>
(example) pageBlock 02: << < 11 12 13 14 15 16 17 18 19 20 > >>
(example) pageBlock 03: << < 21 22 23 24 25 26 27 28 29 30 > >>
...
*/
interface PaginatedList<T> : List<T> { // T는 List가 보유하는 item의 데이터형

    // public property - 인수로 받을 것들 (따라서 자연히 immutable)
    val items: List<T>
    val itemCountPerPage: Int
    val pageCountPerPageBlock: Int

    // public property - immutable
    val totalItemCount: Int
    val totalPageCount: Int
    val totalPageBlockCount: Int

    // public property - private한 setter를 _currentPageNumber 및 _currentPageBlockNumber에 구현
    val currentPageNumber: Int // 1-based indexing
    val currentPageBlockNumber: Int // 1-based indexing

    // getter
    fun getCurrentPageItems(): List<T>
    fun getCurrentPageItemNumbers(): List<Int> // 1-based indexing
    fun getCurrentPageItemsWithNumbers(): List<Pair<Int, T>> // 1-based indexing
    fun getCurrentPageBlockPages(): List<Int>

    // page move method
    fun goToPage(pageNumber: Int): Boolean // 1-based indexing
    fun goToPreviousPage(): Boolean
    fun goToNextPage(): Boolean
    fun goToFirstPage(): Boolean
    fun goToLastPage(): Boolean

    // pageBlock move method
    fun goToPageBlock(pageBlockNumber: Int): Boolean // 1-based indexing
    fun goToPreviousPageBlock(): Boolean
    fun goToNextPageBlock(): Boolean
    fun goToFirstPageBlock(): Boolean
    fun goToLastPageBlock(): Boolean

    // method for cheking before page/pageBlock moving
    fun canGoToPage(pageNumber: Int): Boolean // 1-based indexing
    fun canGoToPageBlock(pageBlockNumber: Int): Boolean // 1-based indexing

    // pageBlock move button visibility method
    fun previousPageBlockButtonVisibility(): Boolean
    fun nextPageBlockButtonVisibility(): Boolean
    fun firstPageBlockButtonVisibility(): Boolean
    fun lastPageBlockButtonVisibility(): Boolean

    // parent interface implementation - List
    override val size: Int
        get() = items.size

    override fun contains(element: T): Boolean {
        return items.contains(element)
    }

    override fun containsAll(elements: Collection<T>): Boolean {
        return items.containsAll(elements)
    }

    override fun get(index: Int): T {
        return items[index]
    }

    override fun indexOf(element: T): Int {
        return items.indexOf(element)
    }

    override fun isEmpty(): Boolean {
        return items.isEmpty()
    }

    override fun iterator(): Iterator<T> {
        return items.iterator()
    }

    override fun lastIndexOf(element: T): Int {
        return items.lastIndexOf(element)
    }

    override fun listIterator(): ListIterator<T> {
        return items.listIterator()
    }

    override fun listIterator(index: Int): ListIterator<T> {
        return items.listIterator(index)
    }

    override fun subList(fromIndex: Int, toIndex: Int): List<T> {
        return items.subList(fromIndex, toIndex)
    }
}

왠만한 상황에 다 대처할 수 있기를 바라며 짰다.

 

#2-2 인터페이스 구현

// by interfacer_han (https://kenel.tistory.com/327)

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
import kotlin.math.min

data class PaginatedListImpl<T>(
    // public property - 인수로 받을 것들 (따라서 자연히 immutable)
    override val items: List<T>,
    override val itemCountPerPage: Int,
    override val pageCountPerPageBlock: Int
) : PaginatedList<T> {

    // public property - immutable
    override val totalItemCount: Int = items.size

    override val totalPageCount: Int =
        if (totalItemCount % itemCountPerPage == 0) totalItemCount / itemCountPerPage
        else totalItemCount / itemCountPerPage + 1

    override val totalPageBlockCount: Int =
        if (totalPageCount % pageCountPerPageBlock == 0) totalPageCount / pageCountPerPageBlock
        else totalPageCount / pageCountPerPageBlock + 1

    // public property - private한 setter를 _currentPageNumber 및 _currentPageBlockNumber에 구현
    private var _currentPageNumber by mutableIntStateOf(1)
    override val currentPageNumber: Int
        get() = _currentPageNumber

    private var _currentPageBlockNumber by mutableIntStateOf(1)
    override val currentPageBlockNumber: Int
        get() = _currentPageBlockNumber

    // getter
    override fun getCurrentPageItems(): List<T> {
        val startIndex = (currentPageNumber - 1) * itemCountPerPage
        val endIndex = min(startIndex + itemCountPerPage, totalItemCount)
        return if (startIndex < endIndex) items.subList(startIndex, endIndex) else emptyList()
    }

    override fun getCurrentPageItemNumbers(): List<Int> {
        val startIndex = (currentPageNumber - 1) * itemCountPerPage
        val endIndex = min(startIndex + itemCountPerPage, totalItemCount)
        return if (startIndex < endIndex) (startIndex + 1 until endIndex + 1).toList() else emptyList()
    }

    override fun getCurrentPageItemsWithNumbers(): List<Pair<Int, T>> {
        val startIndex = (currentPageNumber - 1) * itemCountPerPage
        val endIndex = min(startIndex + itemCountPerPage, totalItemCount)
        return if (startIndex < endIndex) {
            items.subList(startIndex, endIndex).mapIndexed { index, item ->
                (startIndex + index + 1) to item
            }
        } else {
            emptyList()
        }
    }

    override fun getCurrentPageBlockPages(): List<Int> {
        val firstPageInBlock = (currentPageBlockNumber - 1) * pageCountPerPageBlock + 1
        val lastPageInBlock = min(currentPageBlockNumber * pageCountPerPageBlock, totalPageCount)
        return (firstPageInBlock..lastPageInBlock).toList()
    }

    // page move method
    override fun goToPage(pageNumber: Int): Boolean {
        if (canGoToPage(pageNumber)) {
            _currentPageNumber = pageNumber
            _currentPageBlockNumber = ((pageNumber - 1) / pageCountPerPageBlock) + 1
            return true
        }
        return false
    }

    override fun goToPreviousPage(): Boolean = goToPage(currentPageNumber - 1)

    override fun goToNextPage(): Boolean = goToPage(currentPageNumber + 1)

    override fun goToFirstPage(): Boolean = goToPage(1)

    override fun goToLastPage(): Boolean = goToPage(totalPageCount)

    // pageBlock move method
    override fun goToPageBlock(pageBlockNumber: Int): Boolean {
        return if (canGoToPageBlock(pageBlockNumber)) {
            _currentPageBlockNumber = pageBlockNumber
            val firstPageInBlock = (pageBlockNumber - 1) * pageCountPerPageBlock + 1
            _currentPageNumber = firstPageInBlock
            true
        } else {
            false
        }
    }

    override fun goToPreviousPageBlock(): Boolean = goToPageBlock(currentPageBlockNumber - 1)

    override fun goToNextPageBlock(): Boolean = goToPageBlock(currentPageBlockNumber + 1)

    override fun goToFirstPageBlock(): Boolean = goToPageBlock(1)

    override fun goToLastPageBlock(): Boolean = goToPageBlock(totalPageBlockCount)

    // method for cheking before page/pageBlock moving
    override fun canGoToPage(pageNumber: Int): Boolean {
        return pageNumber in 1..totalPageCount
    }

    override fun canGoToPageBlock(pageBlockNumber: Int): Boolean {
        return pageBlockNumber in 1..totalPageBlockCount
    }

    // pageBlock move button visibility method
    override fun previousPageBlockButtonVisibility(): Boolean = currentPageBlockNumber > 1

    override fun nextPageBlockButtonVisibility(): Boolean = currentPageBlockNumber < totalPageBlockCount

    override fun firstPageBlockButtonVisibility(): Boolean = previousPageBlockButtonVisibility()

    override fun lastPageBlockButtonVisibility(): Boolean = nextPageBlockButtonVisibility()
}

_currentPageNumber 및 _currentPageBlockNumber를 State로 두는 것이 중요하다. State가 아니라 일반 Int로 두면, 그 값이 변해도 Jetpack Compose의 Recomposition이 유발되지 않는다. 물론, 안드로이드에서 사용할 것이 아니라면 Int로 둬도 되겠지만 말이다. 

 

#3 검증을 위한 샘플 앱 

#3-1 개요

토이 프로젝트로 #2-2를 검증하기 위한 앱을 뚝딱 만들었다.

 

#3-2 목표

#2-2의 3가지 인수 items: List<T>, itemCountPerPage: Int, pageCountPerPageBlock: Int을 사용자로부터 입력받아 Pagination 객체를 만들고, 그 객체의 프로퍼티를 시각적으로 보인다. 또, 멤버 함수들도 잘 실행되는지 확인한다.

 

#3-3 구조

Jetpack Compose에 MVVM을 적용했다. 왠만하면 다른 라이브러리들을 지양하려고 했지만, 코드가 생각보다 방대해지는 것 같아 Hilt까지 이용해 코드량을 약간 줄였다. 전체 소스코드는 #3-6에 있다.

 

#3-4 스크린샷

테스트 케이스를 다양하게 쳐봤는데, 아직 런타임 에러가 나지는 않았다.

 

#3-5 추가 스크린샷

간단한 앱이지만, 꽤나 알차게 만든 것 같다. 이런 부분은 의식하지 않아도 습관적으로 구현하는 프로그래머가 되고 싶다.

 

#3-6 전체 소스코드

 

android-practice/playground/Pagination at master · Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com

 

#3-7 APK

Pagination.apk
9.70MB

#2-2를 가져다가 쓰고 싶은데, 잘 작동하는지 미심쩍은 사람이 있을 것 같다. 동시에 안드로이드 스튜디오까지 키기는 귀찮은(?) 사람 말이다. 그런 사람들을 위해, #3-6으로 만든 APK 파일이다. 한번 설치해 실행해보자.