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

Flow는 LiveData와 생명주기가 좀 다른가요?

0 추천

변경한 것이라곤 LiveData에서 FLow로 변경한것 밖에 없는데,

제가 아직 Flow에 대해서 잘 몰라서 그런것인지 잘못알고있는게 있어서 그런지..

화면을 이동했다가 다시오면 해당 데이터가 그대로 존재합니다.

화면간 이동은 Navigation으로 합니다.

 

예를들어 상세화면에서 List에 데이터를 눌러 추가하고 완료버튼을 눌러 다른 화면으로 갔다가

다시 상세화면으로 오면 List가 처음부터 시작하는게 아니라 이전 List에 추가로 데이터가 들어갑니다.

즉 화면을 이동했다가 오면 List size가 처음부터 시작되는게 아니라 이전 List의 사이즈부터 시작합니다.

바꾼건 Live data에서 FLow밖에 없는데 왜이런걸까요.. 

 

DetailFragment

class WriteDetailFragment :  Fragment() {
    ...
    
    private val viewModel: WriteDetailViewModel by viewModels {
        WriteDetailViewModelFactory(
            (requireActivity().application as WorkoutApplication).writeDetailRepo
        )
    }

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

        binding.apply {
            // 세트 추가
            addSet.setOnClickListener {
                viewModel.addSet()
            }
        }
        
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.items.collect { list ->
                adapter.submitList(list)
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

 

ViewModel

class WriteDetailViewModel(
    private val repository: WriteDetailRepository
    ): ViewModel() {

    private var _items: MutableStateFlow<List<WorkoutSetInfo>> = MutableStateFlow(repository.getList())
    val items = _items.asStateFlow()

    fun changeUnit(unit: WorkoutUnit) {
        repository.changeUnit(unit)
        _items.value = repository.getList()
    }

    fun addSet() {
        viewModelScope.launch(Dispatchers.IO) {
            repository.add()
            _items.value = repository.getList()
        }
    }
}

 

Repository

class WriteDetailRepository(val dao: WorkoutDao) {
    private var setInfoList = arrayListOf(WorkoutSetInfo(set = 1)) // 세트 1개는 고정
    private var updatedList = listOf<WorkoutSetInfo>()
                get() = setInfoList.toList()

    fun changeUnit(unit: WorkoutUnit) {
        updatedList = setInfoList.map { setInfo ->
            setInfo.copy(unit = unit)
        }
    }

    fun add() {
        val item = WorkoutSetInfo(set = setInfoList.size + 1)
        setInfoList.add(item)
        updatedList = setInfoList.toList()
    }
    
    fun getList() : List<WorkoutSetInfo> = updatedList
}

 

 

Application

class WorkoutApplication : Application() {
    val database by lazy { WorkoutDatabase.getDatabase(this) }
    val workoutListRepo by lazy { WorkoutListRepository(database.workoutDao()) }
    val writeDetailRepo by lazy { WriteDetailRepository(database.workoutDao()) 
}

 

 

codeslave (3,940 포인트) 님이 2022년 12월 27일 질문
codeslave님이 2022년 12월 29일 수정

4개의 답변

0 추천
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.items.collect { list ->
                adapter.submitList(list)
            }
        }
    }

위의 라인이 문제가 되는 것으로 추측이 되구요. LiveData와는 다르게 Flow는 라이프싸이클에 따른 oberver를 활성/비활성화하는 처리를 자동으로 해주지 않습니다. Flow가 Android를 위해 만들어진 것이 아니기 때문에, 라애프싸이클에 대한 추가적인 처리가 필요합니다. 아래 블로그를 읽어보시면 왜 그리고 해결방법에 대한 설명이 자세하게 나옵니다.

https://hongbeomi.medium.com/%EB%B2%88%EC%97%AD-android-ui%EC%97%90%EC%84%9C-flow%EB%A5%BC-%EC%88%98%EC%A7%91%ED%95%98%EB%8A%94-%EC%95%88%EC%A0%84%ED%95%9C-%EA%B8%B8-bd8449e67ec3

spark (227,830 포인트) 님이 2022년 12월 27일 답변
저도 읽어보고 이게 원인일것같다해서 해봤는데 안되네요ㅠ
0 추천

해당 원인은 아무래도 WriteDetailRepository에 있는 것 같습니다. WriteDetailRepository가 Singleton 이라서 WriteDetailViewModel에서 addSet()를 호출하게 되면 

items.value = repository.getList()

repository에는 데이터가 여전히 남아있는 상태이기 때문일 가능성이 많아 보이네요.  이 경우라면 WriteDetailRepository를 Singleton 으로 사용하지 않거나 WriteDetailViewModel에서 WriteDetailRepository에 있는 리스트들을 초기화 해주어야 할 것같습니다.

class WriteDetailViewModelFactory() : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return WriteDetailViewModel(WriteDetailRepository()) as T
    }
}
spark (227,830 포인트) 님이 2022년 12월 28일 답변
감사합니다 선생님. 선생님 답변보기전에는 버튼을 누르면 리스트를 clear()해주는 방식으로 임시방편으로 해결했는데, 선생님처럼 한줄만 바꾸니 코드가 더 간결해지고 좋네요.

그런데 궁금한점에. Application 클래스는 싱글톤으로 작용을하는데,
저 코드가 어떻게 매번 생성이 가능하게 되는것이죠? 싱글톤이면 멤버까지 전부 싱글톤 멤버가 될텐데 말이죠.

get()이 정확히 어떻게 동작하길래 저게 가능한지 궁금합니다. 아직 kotlin 문법도 모르는게 많네요
val writeDetailRepo:  WriteDetailRepository
        get() = WriteDetailRepository(database.workoutDao())

위의 코드를 자바로 변환하면 아래와 같이 됩니다.

public WriteDetailRepository getWriteDetailRepo() {
     return WriteDetailRepository(database.workoutDao());
}
음 get() getter() 메소드로 변환된다는건 알고 있었는데요, getter 라는 메소드를 호출하는 형식이 되니까 싱글톤 함수가 아니므로 매번 호출된다 이말씀인건가요?
네. 맞아요. writeDetailRepo를 호출할 때 자바로 컴파일 된 코드는 getWriteDetailRepo()로 변환이 됩니다.
감사합니다 하나 알아가네요. 죄송한데 글의 질문이랑은 다른질문이지만 추가적으로 질문좀 드릴게요.

본문 코드를 업데이트(VIewModel, Repository,)했습니다.

changeUnit() 부분인데, 토글버튼을 눌러 현재 화면에 보이는 리스트에 표시된 단위(unit)을 토글 버튼 따라 바꿔주는 그런 코드인데, repository에서
토글버튼에 따라 값이 다 정확하게 확인을 했는데 화면에서 업데이트 되질 않습니다.

디버그해보니 repository에서 unit 값이 바뀐 리스트가 리턴되는건 확인했는데
viewmodel의 _items.value = 에서 문제가 있는것같습니다. value 하는데서 에러는 안나는데 여기서 값이 안들어가는것인지 그래서 화면이 업데이트가 안되네요
0 추천

changeUnit 문제는 코드를 잘 보시면 추측이 될 듯 합니다.

fun changeUnit(unit: WorkoutUnit) {
        updatedList = setInfoList.map { setInfo ->
            setInfo.copy(unit = unit)
        }
    }

현재 코드를 보시면 unit을 바꾼 결과로 updatedList를 얻데이트하고 있죠.  그런데 add 함수를 보시면 add 한 결과를 setInfoList와 updatedList에 반영하고 있습니다. 

fun add() {
        val item = WorkoutSetInfo(set = setInfoList.size + 1)
        setInfoList.add(item)
        updatedList = setInfoList.toList()
    }

즉, add changeUnit을 호출하고 나면 wetInfoList와 updatedList가 서로 일치하지 않게 됩니다.

소프트웨어 엔지니어링에서 "source of truth"라고 부르는 개념이 있습니다. 데이터의 원천에 대한 것인데, 이것은 하나가 되야하는게 원칙입니다. 왜냐하면 soure of truth를 한개 이상 가지게 되면 둘 사이의 데이터가 맞지 않게될 가능성이 많아지게 되면, 골치아픈 버그가 될 수 있습니다. 님의 경우가 같은 데이터에 대해 setInfoList 와 updatedList 두개의 source of truth를 가지도록 설계가 되었기 때문에 이런 문제가 발생한다고 볼 수 있습니다.

따라서 해결책은 setInfoList를 하나만 사용하는 구조로 변경하는 것입니다.. 대략 아래처럼 될 겁니다.

class WriteDetailRepository(val dao: WorkoutDao) {
    private val setInfoList = arrayListOf(WorkoutSetInfo(set = 1))
 
    fun changeUnit(unit: WorkoutUnit) {
       setInfoList.map { setInfo ->
            setInfo.copy(unit = unit)
        }
    }
 
    fun add() {
        val item = WorkoutSetInfo(set = setInfoList.size + 1)
        setInfoList.add(item)
    }
     
    fun getList() : List<WorkoutSetInfo> = setInfoList
}

만약 ArrayList를 사용할 때 리스트 어댑터에 문제가 생긴다면, ArrayListn대신에 List를 사용하고 원본을 복사하는 혀애로 바꾸셔야 할겁니다.

spark (227,830 포인트) 님이 2022년 12월 29일 답변
흠 선생님 잘이해가 가지않습니다.

예를들어 setInfoList가 현재 사이즈가 1이라고 하겠습니다(초기상태)

이제 add버튼을 3번 눌러 add()가 3번 호출되고 setInfoList는 사이즈가 4가 되겠죠.
(혹은 누른 횟수만큼 사이즈 추가)

그리고 이 리스트는 사이즈만큼 화면에 보여지게 될것입니다.

이때 토글버튼을 눌러 단위를 바꾸면, changeUnit()이 호출되고
현재 사이즈가 4인 setInfo내의 unit 값을 전부 바꾸게 되고 map { }에의해 새로운 리스트를 반환하고 updatedList에 이 새로운 리스트를 넣게될 것입니다.

새로운 리스트(updatedList)가 생겼으니 ViewModel의 items.value 에 값을 넣어주게 되면 새로운 값이 생겼으므로 Fragment의 Flow에서 반응(?)하고 화면의 단위값을 바꿔줄것이다 라는게 제 생각인데..

updatedList를 굳이 사용하면서 toLIst해준이유가 setInfoList만 리턴시켜주면 ViewModel의 _items가 가진 리스트와 주소 값이 같아져 변화를 감지를 못하는것 같더라구요..

updatedList를 새 리스트를 받는 용도로 ViewModel에서 items.value가 반응하는
용도로 사용했는데.. 이게 잘못된건지 아직 잘모르겠습니다..
그래서 ArrayList를 사용하지 말고 List를 복사해서 사용하라고 한겁니다.  님의 코드 중 아래를 보시면

var updatedList = listOf<WorkoutSetInfo>()
        get() = setInfoList.toList()

updatedList를 읽어올 경우, setInfoList를 가져오기 때문에 결국은 두개가 같은 데이터가 됩니다.

List를 복사해 사용하시거나 아래 링크처럼 DiffUtil을 직접 구현해서 사용하시면 됩니다.
https://medium.com/@chhetri.r4hul/diffutil-a-smarter-way-to-update-items-in-recyclerview-67fa0357dee8
0 추천

Diffutil을 이용해서 아래처럼 처리할 수 있습니다. 이 때는 List 크기를 직접 비교하므로, ArrayList를 사용해도 상관없습니다.

class WorkoutDiffCallback(
    private val oldList: List<WorkoutSetInfo>,
    private val newList: List<WorkoutSetInfo>
) : DiffUtil.Callback() {
    override fun getOldListSize(): Int = oldList.size

    override fun getNewListSize(): Int = newList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return newList[newItemPosition] == oldList[oldItemPosition]
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return newList[newItemPosition] == oldList[oldItemPosition]
    }
}

class WorkoutAdapter(
    private val dataSet: ArrayList<WorkoutSetInfo> = arrayListOf()
) : RecyclerView.Adapter<WorkoutViewHolder>() {

    fun submitList(items: List<WorkoutSetInfo>) {
        val petDiffUtilCallback = WorkoutDiffCallback(dataSet, items)
        val diffResult = DiffUtil.calculateDiff(petDiffUtilCallback)
        dataSet.clear()
        dataSet.addAll(items)
        diffResult.dispatchUpdatesTo(this)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WorkoutViewHolder {
        ...
    }

    override fun onBindViewHolder(holder: WorkoutViewHolder, position: Int) {
        ...
    }

    override fun getItemCount(): Int {
        return dataSet.size
    }
}
spark (227,830 포인트) 님이 2022년 12월 30일 답변
계속 이해 못했는데 get()에서 setInfoList()를 가져온다고 할때 이해한것 같습니다..이걸 못보고 있었으니 걍 제가 바보였네요.

fun changeUnit(unit: WorkoutUnit) {
       updatedList = setInfoList.map { setInfo ->
            setInfo.copy(unit = unit)
        }
    }

그니까 이 코드에서  setInfoList.map으로 각 인덱스마다 새로운 unit 값을 적용한
새로운 리스트가 반환 될것이고 updatedList에 할당 될겁니다.

여기까지의 진행을보면 setInfoList의 unit 값은 당연히 바뀌지 않은 상태겠죠. copy()가 원래 리스트 값을 바꾸지는 않으니까요. 사실 바뀌고 자시고 할것없이 그냥 아무것도 안바뀐 리스트 그대로가 더 맞겠네요
그리고 updatedList에는 바뀐 unit 값이 적용된 새로운 리스트가 들어있을거구요.

그런데
var updatedList = listOf<WorkoutSetInfo>()
        get() = setInfoList.toList()

여기서 get()을 하는데 setInfoList에서 toList()를 하여 새로운 리스트를 가져오면
setInfoList는 여전히 unit값이 바뀌기전 리스트기때문에 updated에 들어가게 될테고, unit 값이 바뀐 새리스트는 다날아가고 다시 원래의 unit 값이 들어있는
setInfoList의 값들이 들어있는 새리스트가 updatedList로 할당되겠네요.

그래서 _items.value에서는 계속 리스트안의 값들이 계속 같은 상태였고
리사이클러뷰에서는 바뀐게 없으니 화면도 변화가 없었던거구요.

맞나요? 아 참고로 DiffUtil은 처음부터 계속 사용하고 있었습니다.

그런데 코드를 좀 달라서 보니 선생님이 사용하신건 그냥 Diff.Callback()을 구현하는 것이고 저는 DIff.ItemCallback()을 구현한게 차이점이네요. 근데 크게 차이점은 없을것같은데 잘모르겠네요
이해하신게 맞아요. DiffUtill.Callback은 이전 아이템과 현재 아이템의 갯수를 리턴하기 때문에 ArrayList를 사용하더라도 상관이 없는데, DiffUtil.ItemCallback은 이 부분이 없기 때문에 ArrayList를 사용할 경우 문제가 될 가능성이 있어요.
감사합니다. 그런데 setInfoList와 updatedList 두개를 사용하지말고 "source of truth" 떄문에 하나만 사용하시고 List를 copy하여 사용하라고 하셨는데 생각해보니 하나만 사용하더라도 add/delete를 하려면 list가 변할수 있어야하기때문에 ArrayList를 사용해야할것같네요.. 이부분은 불가피? 할것같습니다..
...