Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

안드로이드 앱 만들기 : Sqlite 로 구현해 보는 Paging (feat Jetpack compose, 대량 데이터 조회)

Billcorea 2023. 8. 3. 18:05
반응형

오늘 적어 두고자 하는 주제는 Paging입니다.  웹 개발을 하는 경우에는 각종 framework 등을 이용해서 데이터 조회 시 UI의 부하를 줄이기 위해서 Paging을 할 수 있도록 지원을 받습니다. 

 

xml layout 을 구현할 때는 ListView 와 Adapter을 이용해서 별로 고민을 하지 않았던 부분이기도 합니다. 아니면 많은 데이터가 적재될 때까지 사용을 해 보지 않아서 지금 이 순간에는 느려진 화면 때문에 버려진 앱을 개발했던 것일 수도 있기도 하고요.

 

Paging 이란

페이징 기법(paging)은 컴퓨터가 메인 메모리에서 사용하기 위해 2차 기억 장치[a]로부터 데이터를 저장하고 검색하는 메모리 관리 기법이다.기법이다. [1]  가상기억장치를 모두 같은 크기의 블록으로 편성하여 운용하는 기법이다. 이때의 일정한 크기를 가진 블록을 페이지(page)라고 한다. 주소공간을 페이지 단위로 나누고 실제기억공간은 페이지 크기와 같은 프레임으로 나누어 사용한다.  - from wiki 백과

 

이렇게 기술하고 있습니다.  그럼 android 같은 것들은 어떻게 구현을 해야 할까요?  개발자 페이지의 내용을 보면서 구현을 해 보도록 하겠습니다. 

 

https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=ko 

 

페이징 라이브러리 개요  |  Android 개발자  |  Android Developers

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Paging 라이브러리 개요   Android Jetpack의 구성요소 페이징 라이브러리를 사용하면 로컬 저장소에서나 네트워

developer.android.com

 

Gradle 설정 (module)

gradle 설정에 추가한 내용입니다.  이 글을 쓰는 현재는 3.2.0이 최신인 듯합니다.

// Paging 3.0
implementation 'androidx.paging:paging-compose:3.2.0'

 

ListViewSource 예시

다음은 paging source와 view model 정의를 해 보아야 합니다. 다만, 개발자 페이지 등에서 찾을 수 있는 것에는 Sqlite을 활용하는 부분을 찾아보기가 어려웠습니다.   나름 따라 하기를 해 보면서 다음과 같은 코드 등을 구현해 볼 수 있었습니다. 

 


import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
import java.io.IOException

class ListViewSource(pContext : Context) : PagingSource<Int, ViewReceiveList>() {

    val context = pContext

    override fun getRefreshKey(state: PagingState<Int, ViewReceiveList>): Int? {
        return state.anchorPosition
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ViewReceiveList> {

        return try {
            val nextPage = params.key ?: 1
            val viewList = dataReadPage(nextPage, params.loadSize)
            LoadResult.Page(
                data = viewList,
                prevKey = if (nextPage == 1) null else viewList[0].id.toInt(),
                nextKey = if (viewList.isEmpty()) null else viewList[viewList.size - 1].id.toInt()
            )
        } catch (e : IOException) {
            return LoadResult.Error(e)
        }
    }

    @SuppressLint("Range")
    private fun dataReadPage(nextPage: Int, loadSize: Int): List<ViewReceiveList> {
        val returnList = ArrayList<ViewReceiveList>()
        val dbHandler = DBHandler.open(context)
        Log.e("", "readKey=$nextPage")
        val rs = dbHandler.selectRcvList(nextPage, loadSize)
        returnList.clear()
        while (rs.moveToNext()) {
            val viewRevList = ViewReceiveList()
            viewRevList.id = rs.getString(rs.getColumnIndex("_id"))
            viewRevList.strBody = rs.getString(rs.getColumnIndex("strBody"))
            ...
            viewRevList.kakaoProfileImage = rs.getString(rs.getColumnIndex("kakao_image"))
            returnList.add(viewRevList)
        }
        dbHandler.close()
        return returnList
    }
}

 

 DBHandler에서는 select 구문 작성 방법

다음은 여기서  필요한 dbHandler의 select 구문 처리는 어떻게 하는 가입니다.  주의할 부분은 처음 읽어 올때와 다음 페이지로 넘어갈 때 nextKey 을 어떻게 다루어야 하는 가 입니다.  그리고 sqlite에서는 limit을 이용해서 읽어오는 데이터 개수를 조정할 수 있습니다. 그것으로 1 page 분량의 데이터만 조회를 해 오는 것입니다.

 

fun selectRcvList(nextId: Int, loadSize: Int) : Cursor {
    val sql = StringBuffer()
    sql.append("select _id, strBody, inPhoneNumber, chkValue, regDate, eventID, kakao_profile_image from receiveList")
    if (nextId == 1) {
        sql.append(" where _id >= $nextId")
    } else {
        sql.append(" where _id < $nextId")
    }
    sql.append(" order by _id desc")
    sql.append(" limit $loadSize")
    return db.rawQuery(sql.toString(), null)
}

 

DataView 모델에 선언하기

다음은 DataViewModel에서 데이터를 조회하는 구현 부분입니다.  viewModel에서 viewSource으로 전달하면서 context을 전달해 sqlite dbHandler을 사용 시 이용하게 됩니다.

class DataViewModels (application: Application) : AndroidViewModel(application) {

	...
    @SuppressLint("StaticFieldLeak")
    val context = application.applicationContext!!
    val listItemsList : Flow<PagingData<ViewReceiveList>> = Pager(PagingConfig(pageSize = 10)) {
        ListViewSource(context)
    }.flow.cachedIn(viewModelScope)
    
    ...
    
}

 

Compose 화면 구성 하기

이제 리스트 조회하는 Jetpack compose 화면에서의 처리를 보도록 하겠습니다.

화면에서는  LazyColumn을 이용해서 ListView Adapter의 구현과 같이 화면에 데이터를 순서대로 조회하는 구현을 해 볼 수 있었습니다.  이렇게 구현해서  데이터 개수 1만 개가 넘는 sqlite의 데이터 조회를 앱 화면에 무리되지 않는 양으로 paging을 해 조회를 구현해 볼 수 있습니다.


import coil.compose.AsyncImage
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize
import com.google.android.gms.ads.AdView
...
import com.ramcosta.composedestinations.annotation.Destination

@Destination
@Composable
fun ListViewScreen (dataViewModels: DataViewModels) {

    // 데이터 조회 페이지 호출 하기
    val viewLists : LazyPagingItems<ViewReceiveList> = dataViewModels.listItemsList.collectAsLazyPagingItems()

    LazyColumn(
        modifier = Modifier.fillMaxSize(),
    ) {

        items(viewLists.itemCount) {index ->
            DoDisplayItem(item = viewLists[index], index = index)            
        }        
    }

}

@Composable
fun DoDisplayItem(item: ViewReceiveList?, index: Int) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(3.dp)
            .border(1.dp, color = borderLine),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        if (item != null) {
            Row(
                modifier = Modifier.fillMaxWidth().padding(3.dp),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.Start
            ) {
                Text(text = String.format("%s/%s/%d ",item.id, item.eventID, index), style = typography.bodyMedium, modifier = Modifier.padding(3.dp))
            }
            if (item.kakaoProfileImage.isNotEmpty()) {
                Row(
                    modifier = Modifier.fillMaxWidth().padding(3.dp),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.Start
                ) {
                    AsyncImage(
                        model = item.kakaoProfileImage,
                        contentDescription = item.inPhoneNumber,
                        modifier = Modifier
                            .width(60.dp)
                            .height(60.dp)
                            .wrapContentWidth(align = Alignment.Start)
                            .padding(3.dp)
                    )
                    Text(text = item.inPhoneNumber, style = typography.bodyMedium, modifier = Modifier.padding(3.dp))
                }
            } else {
                Text(text = item.inPhoneNumber, style = typography.bodyMedium, modifier = Modifier.padding(3.dp))
            }
            Text(text = item.strBody, style = typography.bodyMedium, modifier = Modifier.padding(3.dp))
            Row(
                modifier = Modifier.fillMaxWidth().padding(3.dp),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.End
            ) {
                Text(text = item.chkValue, style = typography.bodyMedium, modifier = Modifier.padding(3.dp))
                Text(text = item.regDate, style = typography.bodyMedium, modifier = Modifier.padding(3.dp))
            }
        }
    }
}

 

샘플앱 화면 보기

이렇게 구현된 샘플 앱의 이미지 화면입니다.  알림 수신기 앱의  수신 목록 리스트 화면 입니다. 1만 개의 데이터를 다 읽어 드려서  list view을 구현을 한다면 어떻게 동작을 할까는 잘 모르겠습니다. 아무튼 이렇게 구현된 앱의 화면은 부담스럽지 않게 리스트가 스크롤되는 것을 확인할 수 있었습니다.

샘플이미지

 

마무리

sqlite의 데이터 조회는 이렇게 구현해 보았는 데요. 가장 근접한 room을 이용한다고 해 도 그다지 다르지 않을 거라고 생각이 됩니다. online에서 데이터를 가져온다고 해도 큰 문제없이 구현을 해 볼 수 있을 듯합니다. 

 

반응형