오늘 적어 두고자 하는 주제는 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
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에서 데이터를 가져온다고 해도 큰 문제없이 구현을 해 볼 수 있을 듯합니다.
'모바일 앱(안드로이드)' 카테고리의 다른 글
안드로이드 앱 만들기 : 앱에서 챠트 그래프 그리는 도구 (인터넷 펌) (32) | 2023.08.08 |
---|---|
안드로이드 앱 만들기 : The emulator process for AVD Pixel_3a_API_33 has terminated. (AVD 가 실행 되지 않을 때) (22) | 2023.08.07 |
안드로이드 앱 만들기: xml layout 에서 jetpack compose 로 이전 (22) | 2023.08.01 |
안드로이드 앱 만들기 : jetpack compose URL 에서 image 받아와서 보여 주기 (feat coil) (29) | 2023.07.27 |
안드로이드 앱 만들기 : jetpack compose IconButton 만들기에서 쉽게 하는 실수 (?) (8) | 2023.07.26 |