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

코드 제대로 짰는지 확인좀 해주세요

0 추천

조언받고 비즈니스 로직이랑 뷰를 위한 클래스랑 나누면서 최대한 해봤는데 잘 맞는지 모르겠습니다.봐주세요...

Epoxy관련 코드들은 올리면 글자수가 오버되어 뺐습니다 혹시 필요하시면 사진으로..

궁금한것이.. ViewModel 클래스의 addDetail과 deleteDetail 함수에서 궁금한것이 있는데

1.먼저 addDetail()에서 방법1과 방법2로 Detail을 두가지 방법으로 추가할 수있는데요,

방법1같은 경우에는 비즈니스로직이 되는 모델 클래스에서 내부에서 상세 리스트를 가져와서

addDetail()에서 add를 사용하여 추가를 하는것이고 

2. 방법2는 아예 addSubItem을 구현하여 RoutineModel 클래스 내에서 추가하는 방법입니다.

delteDetail()도 마찬가지로 ViewModel에서 삭제하는것과

Model 클래스의 함수에서 삭제하는 로직을 넣는것. 두가지중 어느것이 더 바람직한가요?, 

두가지 다 실행결과 똑같이 정상 동작하기는 합니다...

나머지는 뭐 코드는 제대로 짜였나요..

 

RoutineModel

// 비지니스 로직에 사용될 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("1","test","33")
        routineDetail.toMutableList().add(detail)
    }

    fun getSubItemList() : ArrayList<RoutineDetailModel> = routineDetail

    fun getSubItemSize() = routineDetail.size

    fun addSubItem(item: RoutineDetailModel) { // 방법2
        // 여기다가 구현하는것과 뷰모델에 구현하는것의 차이?
        routineDetail.add(item)
    }

    fun deleteSubItem() {
        // 여기다가 구현하는것과 뷰모델에 구현하는것의 차이?
    }
}

RoutineItem

// 가공된 데이터를 RV(Epoxy)에 나열하기 위한 클래스
sealed class RoutineItem(
    val id: String
) {
    class RoutineModel(
        id: String, // id
        val workout: String, // 운동 종목
        val unit: String, // 무게 단위 (kg or lbs)
//        var routineDetail: List<DetailModel> = listOf() // 단순 RV에 보여주기 위한것이므로 여기서는 상세 프로퍼티가 필요없을듯
    ) : RoutineItem(id)

    class DetailModel(
        val set: String, // 세트
        val reps: String = "1",
        val weight: String
    ) : RoutineItem(set)
}

EpoxyController

class RoutineItemController(
    private val addDetailClicked: (String) -> Unit,
    private val deleteDeatailClicked: (String) -> Unit)
    : EpoxyController() {
    private var routineItem : List<RoutineItem>? = emptyList()

    override fun buildModels() {

        routineItem?.forEachIndexed { index, it -> // it == routineItem
            when(it) {
               is RoutineItem.RoutineModel ->
                   EpoxyRoutineModel_()
                       .id(index)
                       .pos(it.id)
                       .workout(it.workout)
                       .add_listener(addDetailClicked) // setter의 개념으로 초기화하는 듯.
                       .delete_listener(deleteDeatailClicked)
                       .addTo(this)

               is RoutineItem.DetailModel ->
                   EpoxyDetailModel_()
                       .id(it.set)
                       .addTo(this)
           }
        }
    }

    fun setData(items: List<RoutineItem>) {
        routineItem = items
        requestModelBuild()
    }
}

ViewModel

class WriteRoutineViewModel : ViewModel() {
    private val _items: MutableLiveData<List<RoutineModel>> = MutableLiveData(listOf())
    private val rmList = arrayListOf<RoutineModel>()

    val items: LiveData<List<RoutineModel>> = _items // 읽기 전용

    fun addRoutine(workout: String) {
        val rmItem = RoutineModel(UUID.randomUUID().toString(), workout, "TEST")
        rmItem.getSubItemList().add(RoutineDetailModel("2","3","3123"))
        rmList.add(rmItem)

        _items.postValue(rmList) // _items에 저장하며 값이 변화된것을 프래그먼트에서 observe 할수 있도록함
    }

    fun addDetail(pos: String) { // 눌려진 버튼 key 값에 알맞은 위치에 Detail 아이템 추가
        // 모든 아이템은 id를 가지고 있고 고유하기때문에 non-null을 사용해도 될것같다
        rmList.find { it.id == pos }!!.getSubItemList().add(RoutineDetailModel("3","3","23")) // 방법 1
//        rmList.find { it.id == pos }!!.addSubItem(RoutineDetailModel("3","3","23")) // 방법 2

        _items.postValue(rmList) // _items에 저장하며 값이 변화된것을 프래그먼트에서 observe 할수 있도록함
    }
    fun deleteDetail(pos: String) {
        // 모든 아이템은 id를 가지고 있고 고유하기때문에 non-null을 사용해도 될 것 같다.
        val item = rmList.find { it.id == pos }!!

        when(item.getSubItemSize()) { // 상세 아이템의 수가 1개일때 삭제버튼을 누르면, RoutineModel까지 함께 삭제
            1 -> rmList.remove(item)
            else -> item.getSubItemList().removeLast()
        }

        _items.postValue(rmList) // _items에 저장하며 값이 변화된것을 프래그먼트에서 observe 할수 있도록함
    }

    // 뷰를 위한 데이터로 가공하기, 즉 RV에 뿌려질 데이터로 변경, _items의 변화가 감지되면 호출됨
    fun getListItems() : List<RoutineItem> {
        val listItems = arrayListOf<RoutineItem>()

        for(testRM in rmList) {
            listItems.add(RoutineItem.RoutineModel(testRM.id,testRM.workout,testRM.unit))

            val childListItems = testRM.getSubItemList().map { detail ->
                RoutineItem.DetailModel("2","23","55")
            }
            listItems.addAll(childListItems)
        }
        return listItems
    }
}

Fragment

class WriteRoutineFragment : Fragment() {
    private var _binding : FragmentWriteRoutineBinding? = null
    private val binding get() = _binding!!
    private val vm : WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
    private lateinit var epoxyController : RoutineItemController

    override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        _binding = FragmentWriteRoutineBinding.inflate(inflater, container, false)

        epoxyController = RoutineItemController(::addDetail, ::deleteDetail)
        binding.rv.adapter = epoxyController.adapter
        binding.rv.itemAnimator = null // Epoxy 추가삭제 애니메이션 제거

        binding.addExercise.setOnClickListener {
            findNavController().navigate(R.id.action_writeRoutineFragment_to_workoutListTabFragment)
        }
        
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        getTabPageResult() // Tab에서 받아온 정보를 토대로 Routine 추가

        // RecyclerView(Epoxy) Update
        vm.items.observe(viewLifecycleOwner) { updatedItems ->
            epoxyController.setData(vm.getListItems())
        }
    }

    private fun getTabPageResult() {
        val navController = findNavController()
        navController.currentBackStackEntry?.also { stack ->
            stack.savedStateHandle.getLiveData<String>("workout")?.observe(
                viewLifecycleOwner, Observer { result ->
                    vm.addRoutine(result) // 루틴 추가
                    stack.savedStateHandle?.remove<String>("workout")
                }
            )
        }
    }

    private fun addDetail(key: String) {
        vm.addDetail(key)
    }

    private fun deleteDetail(key: String) {
        vm.deleteDetail(key)
    }
}

 

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

1개의 답변

0 추천

리팩토링을 많이 하셨네요. ^^

일단, RoutineModel은 데이터 클래스인데, 실제 하는 일은 MVVM의 모델에 해당하는 코드를 가진 것으로 보이네요. WriteRoutineViewModel도 ViewModel + Model의 역할을 하고 있구요. 엄밀히 말하면 현재 님이 작성하신 코드에서는 Model 의 역할을 WriteRoutineViewModel과 RoutineModel이 나누어서 하고 있어요. 한곳에서만 로직을 가지고 있는 것이 여러모로 유리하다고 생각됩니다. WriteRoutineViewModel로 모든 로직을 위치시키던가, 아니면 별도의 Model 클래스를 두어서 관련 로직을 모드 옮기시면 좋을 것 같습니다.그리고 유닛테스틀 작성해 보세요. 그렇게 하면 리팩토링을 하더라도 기존 로직이 문제없이 돌아가는지 큰 수고없이 확인이 가능하니까요.

예시로, 관련로직을 Repository로 옮긴다면,

class RoutineRepository() {
    private val items = arrayListOf<RoutineModel>()

    fun getItems() : List<RoutineModel> = items
 
    fun addRoutine(routine: RoutineModel): List<RoutineModel> {
        val item = routine.copy(id = generateRoutineId())
        items.add(routine)
        return items
    }

    private fun generateRoutineId(): String = UUID.randomUUID().toString()
 
    fun addDetail(routineId: String, routineDetail: RoutineDetailModel): List<RoutineModel> { 
        // 모든 아이템은 id를 가지고 있고 고유하기때문에 non-null을 사용해도 될것같다
        val item = findItemBy(routineId) ?: throw NoSucheItemFoundException("아이템이 존재하지 않습니다.")
        item.getSubItemList().add(RoutineDetailModel("2","3","3123"))
        item.getSubItemList().add(routineDetail) 
        return items
    }

    fun deleteDetail(routineId: String) {
        // 모든 아이템은 id를 가지고 있고 고유하기때문에 non-null을 사용해도 될 것 같다.
        val item = findItemBy(routineId) ?: throw NoSucheItemFoundException("아이템이 존재하지 않습니다.")
        when(item.getSubItemSize()) { // 상세 아이템의 수가 1개일때 삭제버튼을 누르면, RoutineModel까지 함께 삭제
            1 -> items.remove(item)
            else -> item.getSubItemList().removeLast()
        }
    }

    private fun findItemBy(routineId: String): RoutineModel? =  items.find { it.id == routineId } 
}



class WriteRoutineViewModel (
    private val routineRepository: RoutineRepository
) : ViewModel() {
   ...
}

 

spark (227,530 포인트) 님이 2021년 11월 1일 답변
spark님이 2021년 11월 1일 수정
감사합니다. RoutineModel 클래스가 데이터 클래스인데 MVVM의 Model 역할을 하거나, ViewModel 클래스도 ViewModel + Model의 역할을 하거나 하는 등 이라고 하셨는데.. 제가 아직 그게 어떠한 부분인지 인지 및 구분을 잘 못하기에 몇가지 질문드립니다.

1.제가 MVVM에서의 Model을 자꾸 클래스명을 RoutineModel 등으로 지어서 그런지는 모르겠는데 Model이 `데이터` 와 관련된 일을한다니까 MVVM에서의 Model = data class 로 인식하는 것같은데요. MVVM 에서의 Model은 단순히 data 클래스를 의미하는것이 아니죠?

2.RoutineModel 클래스가 Model의 역할을 한다고하셨는데 getSubItemList(), getSubItemSize(), addSubItem(), deleteSubItem()이 부분때문에 그러신걸까요?
그렇다면 궁금한것이 data class에서는 이러한 메소드같은것이 일체 들어가면 안되나요?

3.제 코드에서 현재ViewModel 에서도 Model을 역할을 한다고 하셨는데 어느부분이 그런걸까요?
제가 생각해본바로는 addRoutine()에서 rmItem.getSubItemList().add() (rmItem을 생성하는 부분도 Model의 역할인지는 모르겠습니다..)
addDetail()도 동일.
deleteDetail()에서 when 부분?
getListItems()는 Model 클래스에서 가져온 데이터를 가공하는부분인데.. 이건 잘모르겠습니다..

3. 리팩토링이 많다고 하셨는데 리팩토링이 정확하게 뭘 의미하는건가요?
제가아는 리팩토링은 그,, 파일 이름 변경이나.. 프로퍼티 이름 한번에 변경하기 이런것의 개념밖에 모르겠네요..



+4.MVVM 개념을 다시 살펴보기 위해 검색하다 본 내용인데..
리포지토리를 뷰모델과 상호작용하기 위해 잘정리된(Clean) 데이터 API를 들고있는 클래스라고 되어있던데요.
'잘 정리된' 이라는 단어 때문에 헷갈려서 그런데 이건 이전에 말씀하셨던 데이터의 가공을 말씀하시는건가요?
만약에 맞다면 데이터의 가공(getListItems())은 뷰모델이 아니라 모델에서 진행되어야하는건가요?
1.
클래스이름 때문이 아니고 MVVM의 M(Model)은 데이터 클래스와는 상관이 없습니다. 여기서 말하는 Model은 비지니스 로직을 처리하는 걸 말합니다. 비지니스 로직은 업무관련 로직을 포함 데이터 처리라고 생각하시면 됩니다. UI 데이터가 아니면 시스템이나 비지니스 데이터가 되겠죠.

2.
네. 그 함수들은 "님의 경우"에는 비지니스 로직에 해당한다고 볼 수 있어요. 따라서 가능하다면 비지니스 로직은 단순한 data class와는 구분을 하는 것이 좋겠죠. data class라는 말을 잘 생각해보시면 data class의 용도가 이해가 가실거예요. (data class = data holding class)

3.
리팩토링은 코드를 고치는 건데, 좀 더 읽기 좋고 관리하기 좋은 에러가 더 적은 코드로 변경하는 걸 말합니다. 알고 보면 별 대단한 말은 아니예요. 마틴파울러의 리팩토링이란 책을 읽어보시면 많은 도움이 될 겁니다.

4.
Repository는 ViewModel에 사용하는 패턴 중의 하나일 뿐이고 ViewMoel 이 Repository를 사용할 필요는 없습니다. UseCase, Interactor 등 다른 패턴 들도 많이 사용되고 상황이나 개발자의 선호도 등에 따라 갈릴 수 있습니다. 모든아키텍쳐에는 정답이 없듯이 말이죠.
ViewModel은 V(View)와 M(Model)사이의 중개자입니다. View에서 발생하는 UI event를 받아서 이걸 적절한 비지 로직을 처리하는 레이어를 통해 데이터를 가져와서 View에 필요한 데이터를 제공해 주는 역할을 합니다.
아키텍쳐의 구성에 따라 달라질 수는 있지만 비지니스 레이어에서는 뷰에 필요한 데이터로 변환을 일반적으로는 하지 않습니다. 왜냐하면 비지니스레이어는  뷰가 어떤 데이터가 필요한지 알 필요가 없기 때문입니다. 이것에 대해 아는 것은 ViewModel이기 때문에 ViewModel에 이런 데이터의 변환이 일어나는 것이 좀 더 자연스럽겠죠.
MVC, MVP, MVI 등이 패턴들도 마찬가지지만 가능하면 레이어간에 종속성을 최소화하는 것이 중요한 원칙 중의 하나입니다. ViewModel이 중간에서 이런 역할을 하는 것이죠.
감사합니다. 죄송한데 코드 수정중에 추가질문이 생겨 질문좀 드릴게요.

1.선생님이 작성한 Repository 코드의 addRoutine(RoutineModel)를 보면
ViewModel에서 생성한 RoutineModel을 받아서 copy를 하시던데, 이렇게 진행하는 이유가 있나요? 그냥 Repository내에서 RoutineModel 아이템을 하나 생성후에 리스트에 넣고 반환하면 안되는지요?
RoutineModel을 생성하는 것은 Model 이 하는 일과는 관련이 없어서 그런건가요?


2.저도 코드를 작성할때는 몰랐는데  _items 및 items 프로퍼티(LiveData)가 필요한지 의문이 생겼습니다.
정확하게는 필요는 한데 _items에 아이템들이 전혀 존재하지 않은채로 계속해서 관찰되고 동작 하고 있습니다.

제가 의도하고 코드들을 작성한건 아닌데요. 코드를 수정하면서 테스트를 해보니
val items: LiveData<List<RoutineModel>> = _items 이 코드가 addRoutine()이 호출될때마다 매번 호출됩니다. 아마 addDetail() 등등 다른 함수들도 마찬가지겠죠.
즉, postValue를 하지 않는 상황에서도 addRoutine()이 호출만 되면 이부분이 계속 호출이 되더라구요. 그래서 _items.postValue(rmList) 부분이 사실상 의미가 없어졌습니다. postValue는 _items에 값의 변화를 주어 관찰하게해서 getListItems를 호출하게 하려는 용도였는데 값의 변화를 주지 않아도 호출되니까요

getListItems()라는 뷰에 뿌릴 데이터 가공에도 LiveData내의 List를 사용하는게 아니라 rmList라는 List를 사용해서 가공을 하기때문에
사실상 _items라는 라이브데이터의 역할이 값도 아무것도 안들어있는데 그냥 observe에 items = _items로 인해 계속 호출되는게 다입니다. 빈값을 계속해서 저장하는거죠..(근데 계속 호출되는것이 정상인지 잘 모르겠습니다)

그래서 의미가 있나 싶은데. 의미를 주기위해서라도 getLIstItems()를 _items를 이용해서 가공하는게 맞는걸까요..
1. 제 코드는 Repository 패턴을 저렇게 사용할 수 있다는 예시를 든 것이지 정답을 보여드린 것 아니예요. 정답은 없어요, 상황에 맞는 최선이 있을 뿐이고, 프로젝트를 매일 접하는 정답은 해당 개발자가 제일 잘 알겁니다. 그리고 RoutineModel을 copy한 이유는, 가능하다면 데이터 클래스는 immutable(불변?)하게 사용할 수 있다면 좋기 때문에 그런 겁니다. 이게 상태값을 변경함으로써 생길 수 있는 문제를 많이 줄여줘요.

2. 안쓸려면 안써도 되긴하는데, 가능하면 사용하는 걸 권장합니다. 왜냐하면 _items를 public으로 만들하면 외부에서도 postValue나 value가 가능하기 때문에, 굳이 불필요한 문제를 일으키지않도록 readonly용 프로퍼티를 하나 선언해서 public으로 만드는게 더 좋은 듯합니다. 좀 번거럽긴 하죠. 개인적으로는 구글의 라이브러리 디자인 실수라고 생각하고 있습니다만.
님의 말대로 rmList 대신 _items.value를 캐시처럼 사용하는 코드를 많이 보긴 했습니다. 그렇게 해도 대부분은 문제가 없을 텐데, 제가 아는 한가지 문제점은 postValue를 여러번 연속 호출할 경우 마지막의 값만 observer에 전달이 됩니다. value는 모든 값이 전달이 되구요. observer의 입장에서는 postValue를 사용하는 것이 문제가 될 일이 거의 없지만, LiveData의 데이터를 캐시처럼 사용하는 경우를 사용해 보면, 한 메소드에서 LiveData.postValue를 여러번 호출할 동안 다른 메소드에서 이걸 가져다 쓰는 경우가 생긴다면 개발자가 기대하는 LiveData.value와 실제 들어 있는 값이 일치하지 않는 문제가 있습니다.
그렇지 않다면 딱히 문제는 없을 거라고 생각하지만, 개인적으로는 디자인적으로는 좋은 모습은 아닌 듯합니다. 차라리 SavedStateHandle(https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate)를 사용해서 처리하는 것이 해당 목적에 더 부합되는 것 같아요.
그리고 LiveData의 역할은 님이 말씀하신 게 맞아요. 뷰에 필요한 데이터만 잘 전달해 주면 되는거죠.
1._items.value를 캐시를 캐시한다는 것은 getListItems에서 말씀하시는지요?
캐시한다는 의미가 뭔지 잘몰라서요...
그러니까 기존에는 getListItems에서 rmList를 가지고 for문을 이용해서 데이터를 가공했다면 제가 하려던 캐시한다는 것은 _items를 value를 이용해서 값을 가지고와서 데이터를 가공하는 행위를 말씀하시나요?

2.그리고 마지막 말씀에 뷰에 필요한 데이터만 잘전달 해주면 된다면..
제가 말씀드린 문제인 라이브데이터에 값이 들어있던 없던 상관은 없다는 말씀인지..
1._items.value를 캐시를 캐시한다는 것은 getListItems에서 말씀하시는지요?
캐시 -> Cache를 말합니다. 데이터를 메모리나 스토리지에 두고 일정 시간 내 또는 변겨이 없을 경우 재사용하는 경우를 말합니다. 어떤 시스템이던 간에 퍼포먼스를 위해 cache를 사용합니다. 다만  안드로이의 경우는 데이터를 처리할 때 제대로 된 cache mechanism을 제공하지 않아서 개발자가 직접 구현하거나 라이브러리 같은 것을 통해서 처리해야 합니다. (IOS의 경우는 기본으로 제공이 되더라구요.) Cache도 알고 보변 별 대단한 용어는 아닙니다.

2. 그리고 마지막 말씀에 뷰에 필요한 데이터만 잘전달 해주면 된다면..
이건 위에서 말씀드린 LiveData.postValue와 관련이 있습니다.  한 메소드에서 LiveData.postValue 연속적으로 호출하는 동안 다른 메소드에서 LiveData.value를 통해 데이터를 읽어오게 되면  읽어오려던 데이터와 실제 LiveData에 들어있던 데이터가 서로 다를 가능성이 있습니다. 이 부분만 문제가 없다면 LiveData.value를 통해 값을 가져와서 사용해도 된다는 말이었습니다.
제가 그 좀 더 궁금해서 검색하다가..저와 비슷한 사례?로 SingleLiveEvent 혹은 SingleLiveData 라는것을 구현해서 사용해보라고 하던데요.
지금 제 상황이 items라는 LiveData가 크게 의미가 없는 상황이잖아요?
그런데 저는 억지로 addRoutine()에서 items.postValue를 하여 값을 집어넣고
getListItems()에서 넣은 아이템 값들을for문을 이용해서 items.value해서 꺼내와서 뷰를 위한 리스트에 나열하고요.

어떻게보면 억지로 value를 함으로써 뷰에서 강제로 observe 하도록 하여 값의 변화를 관찰하도록 한것인데.
https://jungwoon.github.io/android/livedata/2020/11/25/SingleLiveEvent.html
이 링크에서는 저처럼 감지가 필요는 하지만 굳이 크게 값의 의미가 크지 않을때 이것을 사용하던데요..
SingleLiveEvent 클래스를 구현하여서 call()을 호출해서 뷰가 감지하도록 하는 방법이더라구요. 이렇게하면 LiveData의 타입도 굳이 RoutineModel로 설정하지 않아도 되고 아무타입이나 설정해도 되구요.
또한 기존의 arrayList 만으로도 값을 전달해줄 수 도 있구요..
또한 선생님이 말씀하신 for 문에서 items.postValue를 이용해 값을 꺼낼때 생기는 문제 또한 해결할 수 있을것같은데..

어떻게 생각하시나요?
SingleLiveEvent도 LiveData입니다. 이걸 사용하는 이유는 LiveData는 obserer가 deactive(예: onStop)될 때, 자동으로 observer에 이벤트를 전달하지 않습니다.  observer 가 active(예: onStart)될 때 자동으로 LiveData.value에 들어있는 데이터를 전달하게 되어 있습니다. 따라서 에러메세지 처리나 네비게이션 같은 경우 LiveData만을 사용하게 되면 다른 화면에 갔다가 돌아오면 자동으로 같은 이벤트를 타게 되어서 에러메세지가 다시 나오거나 네베게이션의 경우는 이전 화면으로 왔다 갔다 하게 됩니다. 이걸 방지하기 위해서 LiveData에 boolean flag를 넣어서 이미 observe한 경우는, 다시 observe하지 않도록 만든 겁니다. 개인적인 생각으로는 이것도 구글이 개선을 해주면 좋은데 그냥 개발자한테 맡기더라구요. 그래서 어느 똘똘한 개발자가 만들어낸 클래스인데, 많이 쓰이고 있고 저도 사용하긴 합니다. 하지만 최근 들어서 많은 개발자들이 SingleLiveData에 대해 강제로 이벤트를 드랍시켜버리기 때문에 LiveData의 디자인 목적에 맞지않는다고 의문을 제기하곤 합니다.
님의 경우는 SinglLiveData를 사용하시면 안되고 LiveData가 맞습니다.  앞에서도 말씀드렸다시피 LiveData는 observer가 active될 때 value에 있던 데이터를 뷰에 자동으로 전달해주도록 설계가 되었기 때문에, 화면 복구의 기능도 담당합니다. 따라서 LiveData를 잘 디자인해놓으면, 화면 복구가 원할하게 됩니다.
결론적으로 에러메세지, 네비게이션 등의 일회성 메세지가 아닌 경우라면 LiveData를 사용하는 것이 맞습니다. 일회성 메세지의 경우는 SingleLiveData를 사용할 수도 있고, 이미 메세지가 처리되었는지 확인하는 방법과 함께(예를 들면, State machine)사용하는 걸 권장합니다.

참고로 LiveData에서 값을 가져올 때는 postValue가 아니라 value를 사용하셔야 해요.
그리고 LiveData.value에 사용하는 데이터 타입은 실제 업무에서는 뷰의 상태를 가진 포괄적인 데이터 타입으로 사용합니다. 구글의 권장사항도 그렇구요. 즉, 예를 들면,

data class UiState(
   val isDataLoading: Boolean = false,
   val errorMessage: String? = null,
   val listItems: List<ListItem>  = emptyList()
)

위처럼 화면의 요소를 그룹지어서 데이터 클래스를 만들고 이걸 postValue 합니다. 그리고 상황에 따라 data class의 copy 를 사용해 상태를 변경합니다. 아래 처럼요.

private val uiStateLiveData = mutableLiveData<UiState>(UiState())

uiStateLiveData.postValue(uiStateLiveData.value!.copy(isDataLoading = true))

uiStateLiveData.postValue(uiStateLiveData.value!.copy(isDataLoading = false, errorMessage = "에러 발생"))
 
uiStateLiveData.postValue(uiStateLiveData.value!.copy(isDataLoading = false, errorMessage = null, lsitItems = getListItems())

이유는 회사들에서 사용하는 업무용 앱은 한 화면에 처리해야 할 요소가 많기 때문에 뷰 하나마다 LiveData를 가져가게 되면 ViewModel이 금방 관리하기 힘들어질 정도로 복잡해지기 때문에 관련뷰끼리 그룹을 짓는 거죠.
이런 식의 구조는 Jetpack Compose를 사용해도 동일합니다.
그러니까..vm.items.observe() { } (통칭 observe)는 프래그먼트가 프래그먼트같은 화면이 재생성 될때 자동으로 items.value를 하여 값을 불러와 화면을 복구하는 기능이 있는데..
제가 만드는것이 루틴을 추가할때 다른 화면으로 넘어간후 돌아오면서 받아오는 화면 전환이 이루어지기때문에 저의경우 화면복구가 가능한 LiveData를 사용하는것이 맞다는 말씀인가요?

그런데 선생님이 내비게이션에는 화면전환으로 왔다갔다할시 같은 이벤트를 타기때문에 LiveData만을 사용하는것이 좋지 않다고 말씀하셨는데요, 제가 지금 내비게이션으로 화면전환을 사용하여 데이터를 받아오는데..

오히려 LiveData를 사용하면 안되고 SingleLiveData를 사용해야하는것 아닌가요?
헷갈리네요 ㅠㅠ
...