마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

리사이클러뷰 아이템 위치 파악에서 아이디를 이용한 비교

0 추천

// 비지니스 로직에 사용될 Model 클래스
data class RoutineModel(
    val id: String, // 고유 id (UUID 사용)
    val workout: String, // 운동 종목
    val unit: String, // 무게 단위 (kg or lbs)
) {
    private var routineDetail: ArrayList<RoutineDetailModel> = arrayListOf()
    init {
        // 루틴 생성시과 동시에 1개의 상세 아이템을 가지고 있게 하기 위함
        val detail = RoutineDetailModel(UUID.randomUUID().toString(),1,"")
        routineDetail.add(detail)
    }

    fun getSubItemList() : ArrayList<RoutineDetailModel> = routineDetail

    fun getSubItemSize() = routineDetail.size
}
data class RoutineDetailModel(
    val id: String = UUID.randomUUID().toString(), // UUID
    val set: Int, // 세트
    var weight: String ="",
    val reps: String = "1") {
}

 

// 가공을 위한 RoutineItem클래스

// 가공된 데이터를 RV(Epoxy)에 나열하기 위한 클래스
sealed class RoutineItem(
    val id: String,
    val layoutId: Int
) {
    class RoutineModel(
        id: String, // Id, UUID 사용
        val workout: String, // 운동 종목
        val unit: String, // 무게 단위 (kg or lbs)
//        var routineDetail: List<DetailModel> = listOf() // 여기다가 넣는다면?
    ) : RoutineItem(id, VIEW_TYPE) {
        companion object {
            const val VIEW_TYPE = R.layout.item_routine
        }
    }

    class DetailModel(
        id: String,
        val set: Int, // 세트
        var weight: String,
        val reps: String = "1",
    ) : RoutineItem(id, VIEW_TYPE) {
        companion object {
            const val VIEW_TYPE = R.layout.item_routine_detail
        }
    }
}

 

 // RecyclerView에 보여줄 형태로 RoutineModel 리스트를 가공

fun getListItems() : List<RoutineItem> {
        val listItems = arrayListOf<RoutineItem>() // 에폭시에 나열하기 위한 가공된 최종 리스트

        for(tempRm in _items.value!!) {
            listItems.add(RoutineItem.RoutineModel(tempRm.id,tempRm.workout,tempRm.unit))

            val childListItems = tempRm.getSubItemList().map { detail ->
                RoutineItem.DetailModel(detail.id, detail.set, detail.weight,"55")
            }
            listItems.addAll(childListItems)
        }
        return listItems
    }

 

즉 그러니까 RoutineModel 클래스와 RoutineModel내에 잇는 Detail들을 가공하여

List<RoutineItem> 형태로 만들어 펼치는 겁니다. 이렇게해서 이 리스트를 ㄹ리사이클러뷰에 뿌리는 형태인데,

 

이제 아이템을 추가 및 삭제할때가 문제인데요, 이전에는 아이템마다 UUID를 사용해서

비교했었는데 굳이 Adapter내에 bindingAdapterPosition를 냅두고 id를 비교해야하여 위치를 찾고 Detail아이템을 추가 및 삭제할 필요가 있나 의문듭니다.

어댑터의 bindingAdapterPostion을 사용하면 혹시 모를잘못된 위치?를 파악하지 않아도 될거고.. id 비교도 굳이하지 않아도 된다는 점이 즉 안정성 측면?이 이점일거같은데요..

 

단점은 위에서 말했다시피 List<RoutineModel>에 존재하는 데이터들을 펼쳐서(가공해서) List<RoutineItem>만들었고 이것이 곧 리사이클러뷰에 보여질 리스트들인데요, 

만약에 bindingAdapterPosition을 사용하면 아이템을 클릭했을때 이 아이템들의 위치가 반환되겠죠,

그런데 정작 아이템이 추가되고 삭제되는것은 List<RoutineModel>이라 이 반환된 위치값과

RoutineModel에 존재한 아이템의 위치가 알맞지 않다는 것입니다.

그래서 이것때문에 결국 UUID를 사용해야하나 하는 생각이드는데 어떻게 하는것이 올바를까요?

 

++)Repository에서 데이터를 가공해도 될까요?

codeslave (3,940 포인트) 님이 2021년 12월 11일 질문
codeslave님이 2021년 12월 11일 수정

1개의 답변

0 추천
결정을 하세요. 포지션을 사용하던, ID를 사용하던 님의 앱의 요구사항에 전적으로 달린 겁니다. 그건 님만이 결정할 수 있어요. 포지션으로 충분히 처리가 가능하면 그렇게 하시면 되구요, 문제가 있다면 ID로 하셔야 되구요.

각 결정에는 트레이드 오프가 있습니다. 어떤 것이 좋으면 다른 것은 안좋을 수 있어요. 뭐든지 잘 들어맞는 완벽한 솔루션은 잘 존재하지 않습니다. 복잡도가 덜한 방향으로 결정을 하셔야 합니다. 복잡도는 코드량과는 좀 관련이 없어요. 코드가 이해가 더 잘가고, 수정이 손쉬워야 합니다.

Repostiory의 경우는 도메인 데이터 타입을 리턴한다면 상관이 없을 것 같습니다. 대신 뷰에서 사용하는 데이터 타입을 리턴하고 싶다면 맵퍼 클래스나 뷰모델에서 처리해야 합니다.
spark (227,530 포인트) 님이 2021년 12월 11일 답변
spark님이 2021년 12월 11일 수정
제가 보기에 복잡도가 증가하는 원인은 Database나 Repository에서 리턴되는 데이터 구조의 복잡도가 상당부분 영향을 미친다고 보여집니다. 사실 화면에 보여지는 데이터는 정해져 있습니다. 따라서 화면에 보여질 데이터를 가공하기 쉽게 하려면 데이터베이스로부터 나오는 데이터의 구조가 가공하기 쉬운구조여야 할 겁니다.

현재는 parent아이템에 child아이템이 리스트로 존재하므로 좋은 점도 있지만, 님이 겪는 문제점도 발생합니다.
따라서, Repository 쪽에서 아예 child 데이터만 가지고 처리하도록 해줄 수도 있습니다. 대신 RoutineDetailModel은  RoutineModel 대한 정보를 가지고 있어야 겠죠.

data class RoutineDetailModel(
    val ...
    val parent: RoutineModel
)

이렇게 하면 굳이 뷰와 Repository사이에 데이터 구조의 차이가 별로 존재하지 않게 되므로, 리사이클러뷰에 맞는 데이터로의 전환이 수월해질 수 있을겁니다. 이게 답은 아닐 수도 있지만, 제 포인트는 데이터 구조를 잘 리뷰하셔서 최적화된 구조를 가져가라는 겁니다.

지난 번 질문하셨을 때 언급했던 Task예제를 다시 잘 확인해보세요. 그 질문 올리셨던 분은 RoomDB에서 Task들만 다 가져와서 그룹핑을 해서 처리했었죠. 별도로 그룹자체를 가지고 처리를 하지는 않았어요. Room에서는  Relationship 통해 자식 엔터티가 부모 엔터티를 조인해서 데이터를 가져오기가 쉽게 되어 있습니다. 님이 자식 데이터를 다루어야 한다면, 자식 데이터에서 부모데이터를 참조하는 형태가 부모데이터에서 자식데이터 리스트를 가지고 처리하는 것보다 핸들링이 더 쉽다는 걸 느끼실거예요.
StickyHeader를 찾아보세요. 자식 아이템만 가지고도 그룹핑이 가능합니다. 아래 링크에서 보는 것과 같은 결과가 나타납니다. (리스트를 자동 그룹핑해줍니다.)
https://jitpack.io/p/smhdk/km-recyclerview-sticky-header
흠 그분이 올리신 질문글을 천천히 읽어보고는 왔는데 이해가 가기도 하고 안가기도 하고 그러네요.

https://www.masterqna.com/android/99317/%EB%A9%B0%EC%B9%A0-%EC%A7%B8-%EC%A4%91%EC%B2%A9%EB%A6%AC%EC%82%AC%EC%9D%B4%ED%81%B4%EB%9F%AC%EB%B7%B0-mvvm%EC%9C%BC%EB%A1%9C-%EA%B3%A8%EB%A8%B8%EB%A6%AC%EB%A5%BC-%EC%95%93%EA%B3%A0-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4-%E3%85%A0%E3%85%A0

-----

이분 글을 읽어보니 Child를 위한 데이터 클래스는 저처럼 따로 두지 않은것같고..(부모와 자식을 따로 구분하지않음?) - 저는 RoutineModel과 DetailModel을 따로 구분했죠,
TaskEntity(부모) 프로퍼티로 Child처리까지 하는것 같은데 맞나요?
Child에 관한 클래스가 안보여서 정확하게 어떻게되는지 모르겠어요.

그리고 DAO에서 TaskEntity까지만 가져온다는것은 알겠습니다.
그런데 그룹화를 한다는 말은 그 아래 선생님이 답변하신 코드중에
toListItems() 메소드가 맞나요? 그 가공한다는 메소드요..


+) 또한 추가 질문으로 선생님이 지금 답변에 Detail 클래스에 Routine 클래스를 프로프티로 뒀는데 이렇게 둔다면 어떠한 장점이 있나요?
저는 당연히 뷰와 같은 구조로 생각해 RoutineModel에 Detail이 들어가야한다고 생각했었는데 말이죠..
데이터베이스를 생각해보시면 이해가 가실거예요.디테일 테이블에서 마스터 테이블을 조인하는 경우말이죠. 님의 경우 데이터를 처리하는 중심은 디테일테이블이기 때문에(조회, 추가, 삭제, 수정), 디테일 테이블을 가지고 핸들링하는게 더 편하다고 보여져요.
그리고 디테일 테이블에서 마스터 테이블을 조인해서 화면에서 사용할 경우, 당연히 같은 마스터 데이터끼리 그룹을 만들어야 헤더를 표시할 수 있겠죠. 님처럼 데이터 구조를 가져갈 수도 있겠지만, 저같은 경우는 말씀드린 방법으로 처리를 하고 있습니다. 그래서 딱히 님이 겪는 문제가 생기지는 않아요.
흠 아직 DB를 제대로 다뤄본적이 없어서 이해가 아주 잘가지는 않네요ㅠ.
선생님, 그런데 그룹화 관해서 말인데요.
제가 현재 저분이나 저나 sealed 클래스의 구조는 리사이클러뷰에 뿌려주기위해 가공을 위한 클래스로 사용하고 있잖아요?
대충 이러한 구조로 말이에요.
sealed class GroupedItem(val layoutId : Int = 1) {
    data class Header(
        val id: String = "",
    ) : GroupedItem(someValue)

    data class Item(
        val id: String = "",
        val set: Int = 1,
    ) : GroupedItem(someValue)
}
그런데 여기서 저는 이전에는 RoutineModel과 RoutineDetailModel을 따로 두고 리스트를 만들고 이 리스트를 RoutineItem.클래스 의 형태로 가공을 했었는데,

차라리 이 Routine, Detail 모델 클래스를 따로 두지않고, RoutineItem 클래스
하나만 활용하는 방안으로
class Group(
    val header: Header = Header()
    val itemList: List<Item> = listOf()
)
이런식으로 그룹화 해서 하는 방법은 별로일까요?
그렇게 하셔도 될거라고 보입니다. 확신이 안가시면 일단 구현방법의 후보군을 좁힌 후 번갈아 가면서 테스팅해보세요 그리고 제일 구현과 코드를 보는데 제일 편한 방법을 선택하세요.
아아 감사합니다. 선생님 이제와서 드리는 질문이라 죄송하지만 그동안 아무생각 없이 코드를 짜서 생각을 안했던건데, 저기 링크에 질문주신분 답글이나 예전에 저한테 코드를 가르쳐주셨을때
sealed class를 활용해서 RecyclerView에 가공하기 위한 클래스를 따로 만드셨잖아요?

그런데 궁금한게 ㅇ저기 질문주신분의 경우에는 TaskEntity라는 데이터 모델이 존재하고 저의 경우에는 RoutineModel과 RoutineDetailModel이라는 데이터 모델 클래스가 따로 존재하는데
굳이 이 데이터 모델클래스와 sealed 클래스를 따로 두는 이유가 있을까요?

바로 sealed 클래스를 활용해서 인스턴스를 생성하면 안되는지요?

그러니까 예를들면 저분의 경우
TasEntity 리스트에서 하나씩 뽑아서 TaskListItem이라는 sealed 클래스로 가공을 하잖아요?
저 같은 경우는 RoutineModel 과 RoutineDetailModel 리스트를 만들어서 RoutineItem이라는 sealed 클래스로 가공하구요.

그런데 굳이 TaskEntity나 RoutineModel 혹은 DetailModel 클래스를 두지않고,
바로 List<TaskEntity>가 아니라 Lilst<TaskItem.Header> 혹은 List<RoutineItem.Header> 이런식으로 sealed 클래스에 바로 접근(?) 하는식으로 아이템(인스턴스)를 생성하는 방식을 사용하면 안되나요?
네, 구체적인 타입으로 생성을 할 수있으면 그게 더 좋죠., ConcatAdapter의 경우는 어댑터 별로 다른 타입을 적용할 수 있지만, 어댑터를 하나만 사용해서 멀티뷰를 처리할 때는, 어댑터에서는 List<TaskItem>과 같이 Generic타입을 사용하기 때문에 그렇게 된 겁니다.  물론
List<Any>같이 사용할 수도 있지만, 그렇게 되면 타입의 안정성도 떨어지고 오히려 코드가 더 지저분해질거예요. 님 생각대로 된다면 그렇게 하는게 더 좋아요.
구체적인 타입생성이 TaskItem 혹은 RoutjmeItem(sealed클래스)가 아닌 TaskEntity RountineModel 등등을 말씀하시는거죠?

말씀대로 인스턴스를 생성할때 TaskItem.Header 나 RoutineItem.Header라든지 이런식으로 인스턴스를 생성한다면, 코드가 많이 지저분 해보일수도 있겠네요..

selaed 클래스.클래스()로 인스턴스를 생성할 수있는데 굳이 왜 data클래스를 두는지 궁금했는데 그런이유가 있군요..

그런데 any클래스같은 경우는 타입안정성이 떨어지겠지만..제 경우에는 사용할 일이 없을거같고 리스트로 List<RoutineItem> 이러한 식으로 한다면 리스트에 추가할때 오히려 타입 안정성이 생기지 않나요?  하위 RoutineItem이라는 sealed클래스의 하위 클래스만 받을수 있으니까요?
...