๊ฐœ๋ฐœ ์ผ์ง€ ๐Ÿ’ป/๊ธฐํƒ€

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 ํŒŒ์ผ์ด๋‹ค. ํ•œ๋ฒˆ ์„ค์น˜ํ•ด ์‹คํ–‰ํ•ด๋ณด์ž.