일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- Android
- php
- Kotlin
- junit
- databinding
- Reactive
- android13
- SWIFTUI
- Observable
- GCP
- rx
- node
- mysql
- 인앱결제
- 동영상
- MediaSession
- Animation
- google play
- MotionLayout
- paging
- RxKotlin
- Koin
- SwiftUI Tutorial
- Android 13
- MediaPlayer
- list
- 테스트 자동화
- PagingLib
- node.js
- mvvm
- Today
- Total
봄날은 갔다. 이제 그 정신으로 공부하자
여러가지 방식으로 List 만들어보기 - part 2 본문
이전 글에서 여러가지 형태의 List 샘플 기초를 만들어보았는데요.
실제 앱을 개발하다보면 이전 글과 같은 단순한 리스트를 만드는 경우는 거의 없습니다.
거의 대부분이 network API를 사용해 리스트 목록을 가져오고 페이징은 기본이면 필요에 따라 DB를 사용하기도 합니다.
또한 리스트 목록 사이사이에 광고가 포함되기도 하지요.
이 글에서는 이러한 다양한 요구사항에 대응할 수 있는 리스트를 만들어 보도록 하겠습니다.
이 글에서는 혼선을 최소화 하고자 Network 부분은 제외하고 설명합니다.
페이징에서 Network API 연동은 아래 글을 참고하시면 됩니다.
https://als2019.tistory.com/22
프로젝트에 ROOM 추가하기
build.gradle에 아래와 같이 ROOM 종속성을 추가합니다.
// build.gradle (app level)
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// build.gradle (project level)
buildscript {
ext {
…
room_version = "2.1.0-alpha06"
}
…
}
Database 생성하기
아래와 같이 @Database, @Entity 어노테이션을 사용해서 데이터베이스를 생성해줍니다.
// MyDatabase.kt
@Database(entities = [DataDefault::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
abstract fun myDao(): MyDao
companion object {
private val DB_NAME = "my-db"
private var instance: MyDatabase? = null
fun getInstance(context: Context): MyDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context)
}
}
private fun buildDatabase(context: Context): MyDatabase {
return Room.databaseBuilder(context.applicationContext, MyDatabase::class.java, DB_NAME)
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
}
}).build()
}
}
}
// DataDefault.kt
@Entity(
tableName = TABLE_NAME,
indices = [Index(value = [INDEX])])
@Parcelize
data class DataDefault(@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "index") var index: Int = 0,
@ColumnInfo(name = "title") var title: String = "",
@ColumnInfo(name = "desc") var desc: String = ""): Parcelable {
constructor() : this(0, "", "")
companion object{
const val TABLE_NAME = "tableDataDefault"
const val INDEX = "index"
}
}
데이터베이스에 접글 할 수 있는 인터페이스 만들기
데이터베이스에 데이터를 추가하고 가져오기 위해 @Dao 어노테이션을 이용해 아래와 같이 interface를 생성해줍니다.
아래 예제는 간단히 데이터베이스에 테이터를 insert & query 샘플 입니다.
// MyDao.kt
@Dao
interface MyDao {
@Query("SELECT * FROM tableDataDefault")
fun getAll(): MutableList<DataDefault>
@Query("SELECT * FROM tableDataDefault WHERE `index` > :offset LIMIT :limit")
fun getDatas(offset: Int, limit: Int): MutableList<DataDefault>
@Query("SELECT COUNT('index') FROM tableDataDefault")
fun getCount(): Int
@Insert
fun insert(dataDefault: DataDefault)
}
헤더가 포함된 리스트 ROOM으로 만들기 (Multi item + ROOM + Coroutines)
이번 예제에서는 ViewModel에서는 아무런 처리도 하지 않고 Coroutines을 사용해서 View에서만 처리를 해보도록 하겠습니다.
별거 없습니다.
ListAdapter 인스턴스 생성 시 데이터베이스에 저장된 모든 데이터를 가져와서 넣어주면 됩니다.
// MultiRoomActivity.kt
class MultiRoomActivity: BaseActivity<ActivityMultiRoomBinding>() {
…
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
…
viewDataBinding.vmMultiRoom?.let {vm->
GlobalScope.launch(Dispatchers.IO) {
rcvMultiRoom.adapter = ListAdapterMultiRoom(MyDatabase.getInstance(this@MultiRoomActivity).myDao().getAll(), vm)
(rcvMultiRoom.adapter as ListAdapterMultiRoom).notifyDataSetChanged()
}
}
}
}
* 주의:
이 방식에는 문제가 있습니다.
데이타베이스에 데이터가 몇개 없으면 성능에는 큰 문제가 없지만 데이터가 추가 될 수록 , 리스트를 초기화하는데 많은 비용을 소모하게 됩니다. 즉, 이러한 방식은 바람직하지 않으므로 많은 수의 데이터가 저장되는 경우에는 아래 방식을 사용하는 것이 좋습니다.
헤더와 광고가 포함된 리스트 ROOM + Paging으로 만들기 (Multi item + ROOM + Paging + Coroutines)
이제 마지막 예제네요.
여기에서는 기존 헤더와 아이템을 표현하는 것에서 추가로 20개에 한개씩 광고 아이템을 추가해보도록 하겠습니다.
즉, 맨 처음에는 헤더가 보이고 그다음부터는 20개 단위로 중간에 광고가 하나씩 추가되는 구조 입니다.
ViewModel에서 페이지 인스턴스를 생성해주고 View에서 ViewModel의 load() 함수를 호출 해 리스트를 초기화 해주는 부분까지는 기본 페이징 라이브러리 예제와 유사합니다.
차이점이 있다면 페이지 인스턴스를 만들 때 파라미터로 Database를 사용한다는 부분 입니다.
이부분은 Koin을 사용하였습니다. (자세한 내용은 하단 링크된 샘플의 MyDi.kt 파일을 참고하시면 됩니다.)
우선 페이지 data source class를 살펴보면 아래와 같습니다.
초기 로드 시 20개의 파일을 DB에서 읽어와서 화면에 보여주는데 기본적으로 맨 앞에 헤더를 한개 추가해주고 20개 단위로 광고를 추가해줍니다. 이는 더보기에서도 마찬가지 입니다.
// RoomPagingDefaultDataSource.kt
class RoomPagingDefaultDataSource private constructor(val database: MyDao): ItemKeyedDataSource<Int, DataDefault>(){
…
var itemKey = 0
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<DataDefault>
) {
GlobalScope.launch(Dispatchers.IO) {
val datas = database.getDatas(params.requestedInitialKey?:0, params.requestedLoadSize)
datas.add(0, DataDefault(-1, "", ""))
if(datas.size > 0) {
itemKey = datas.get(datas.size - 1).index
}
if(datas.size == (params.requestedLoadSize+1)) {
datas.add(DataDefault(-1, "광고 보고 가실께요.[${itemKey}]"))
}
callback.onResult(datas)
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<DataDefault>) {
GlobalScope.launch(Dispatchers.IO) {
val datas = database.getDatas(params.key, params.requestedLoadSize)
if(datas.size > 0) {
itemKey = datas.get(datas.size - 1).index
}
if(datas.size == (params.requestedLoadSize)) {
datas.add(DataDefault(-1, "광고 보고 가실께요.[${itemKey}]"))
}
callback.onResult(datas)
}
}
override fun getKey(item: DataDefault): Int {
return itemKey
}
}
기존 코드와 다른 부분은 getKey() 함수 입니다.
DB에서 데이터를 읽어온 후 맨 뒤에 임의로 아이템을 추가했으므로 getKey()함수의 파라미터로 넘어오는 인스턴스는 맨 나중에
추가한 광고 아이템의 인스턴스가 넘어오게 된 아이템 키가 꼬여버리는 문제가 발생하므로 DB에서 데이터를 읽어온 후 바로 마지막 아이템의 key 값을 별도로 저장해 준 후
getKey() 함수 호출 시 반환해주도록 처리하였습니다.
이제 ListAdapter에서 Header, AD, Item을 처리할 수 있는 3개의 각기 다른 Holder를 만들어주고 각 기준에 맞게 Item이 반환될 수 있도록 처리합니다.
// ListAdapterMultiRoomPaging.kt
class ListAdapterMultiRoomPaging(var vm : MultiRoomPagingViewModel)
: PagedListAdapter<DataDefault, RecyclerView.ViewHolder>(DIFF_CALLBACK){
companion object{
val HEADER = 0
val ITEM = 1
val AD = 2
private val DIFF_CALLBACK: DiffUtil.ItemCallback<DataDefault> =
object : DiffUtil.ItemCallback<DataDefault>() {
override fun areItemsTheSame(oldItem: DataDefault, newItem: DataDefault): Boolean {
return oldItem.index == newItem.index
}
override fun areContentsTheSame(oldItem: DataDefault, newItem: DataDefault): Boolean {
return oldItem.equals(newItem)
}
}
}
class HeaderViewHolder(val binding: HeaderMultiItemBinding) : RecyclerView.ViewHolder(binding.root)
class AdViewHolder(val binding: AdMultiItemBinding) : RecyclerView.ViewHolder(binding.root)
class ItemViewHolder(val binding: ItemMultiRoomPagingBinding) : RecyclerView.ViewHolder(binding.root)
override fun getItemViewType(position: Int): Int {
return if(position == 0){
HEADER
}else if(getItem(position)?.index?:-1 == -1){
AD
}else {
ITEM
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when(viewType) {
HEADER -> HeaderViewHolder(
DataBindingUtil.inflate(
inflater,
R.layout.header_multi_item,
parent,
false
)
)
AD -> AdViewHolder(
DataBindingUtil.inflate(
inflater,
R.layout.ad_multi_item,
parent,
false
)
)
else -> ItemViewHolder(
DataBindingUtil.inflate(
inflater,
R.layout.item_multi_room_paging,
parent,
false
)
)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(getItemViewType(position)){
ITEM -> {
(holder as ItemViewHolder).binding.run {
data = getItem(position)
vmMultiRoomPaging = vm
}
}
AD -> {
(holder as AdViewHolder).binding.run {
data = getItem(position)
}
}
}
}
}
이렇게 처리하는 경우의 장점은 리스트에 추가된 데이터의 갯수와 표현되는 데이터의 갯수가 동일하다는 것 입니다.
이 예제 이전의 예제에서는 헤더 처리를 위해 리스트에 추가된 Item의 갯수보다 리스트에 표현되는 갯수가 1개 더 많았습니다.
1개 정도 많은 건 리스트 관리하는데 큰 문제가 없지만 중간에 광고와 같은 비정형 데이터가 다수 추가되는 경우,
리스트에 추가된 데이터와 표현되는 데이터 갯수가 다르게 되면 관리가 많이 복잡해집니다.
이러한 경우 위의 예제와 같이 페이지 data source에서 데이터를 추가할 때 해당 타입에 맞는 데이터를 추가해주고
List Adapter에서 타입에 맞게 표시해주도록 하면 보다 수월하게 리스트 아이템을 관리할 수 있습니다.
* 참고:
위 설명된 내용에 대한 샘플 소스는 아래 github에 있습니다.
'학습' 카테고리의 다른 글
미디어 세션 사용 - part2 (0) | 2020.12.31 |
---|---|
미디어 앱 아키텍쳐 개요 - part1 (0) | 2020.12.30 |
여러가지 방식으로 List 만들어보기 - part 1 (0) | 2020.12.22 |
Google Play 결제 시스템 - Google Play 결제 라이브러리 통합 테스트 (0) | 2020.12.18 |
Google Play 결제 시스템 - 프로모션 코드 (0) | 2020.12.18 |