달달한 스토리

728x90
반응형

출처 핀터레스트

 

상황은 이렇습니다. 다음과 같이 3개의 EditText가 있고,

 

이 EditText의 글씨가 모두 써졌으면,

 

아래 버튼이 활성화되게끔 하려고 합니다.

 

 

 

우선 저는 상태에 따른 모델 값이 필요하다고 생각하여 다음과 같이

 

UiState를 만들었습니다.

 

UiState.kt

data class UiState(
    val type: EditType? = null,
    var editState: EditState = EditState.EMPTY
) {
    enum class EditState {
        EMPTY,
        WRITTEN
    }
    enum class EditType {
        NAME,
        ADDRESS,
        PHONENUM
    }
}ㅇ

 

UiState는 

 

type: EditType ->  EditText의 들어갈 내용에 대한 타입입니다. (NAME, ADDRESS, PHONENUM)

editState: EditState -> EditText가 isEmpty라면 EMPTY라는 enum값을 가지고 text가 하나라도 써져있다면

WRITTEN 값을 가집니다.

 

editState의 기본값은 빈 값이니 EditState.EMPTY 값을 주었습니다.

 

그다음 볼 것은 EditText의 TextWatcher 리스너를 달고,

 

string값을 옵저빙 하여 받기 위해

 

확장 함수를 사용했습니다.

 

EditTextd의 Flow를 반환받는 확장 함수를 만들었습니다.

 

Extensions.kt

import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.widget.EditText
import com.example.flow1.ui.widget.UiState
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.onStart

object Extensions {

    fun EditText.textChangesToFlow(editType: UiState.EditType): Flow<UiState?> {
        // callbackFlow : callback으로 반환받은 데이터를 flow로 converting 해줍니다.
        // 내부적으로 channel을 생성하는 ProducerScope 입니다.
        // 고로, 데이터 전달을 trySend로 할 수 있습니다. (기존의 offer)
        // 하나의 flow를 사용할 수 있는 builder라고 보셔도 됩니다.
        // 내부적으로 보시게 되면 buffer는 기본값으로 64개를 사용하게 되는데,
        // buffer에 데이터가 넘어갈 때 처리하는 옵션으로 onBufferOverflow 인자가 있는데,
        // 기본적인 값으로 BufferOverflow.SUSPEND로 되어있습니다. 이 옵션은 channel에 data가 64개가 넘어가면,
        // suspending 처리가 되어가 send가 block됩니다.
        return callbackFlow {
            val listener = object : TextWatcher {
                override fun afterTextChanged(s: Editable?) = Unit
                override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
                override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) {
                    // 기존에 offer은 이제 Deprecated가 되었고, trySend로 대체된다.
                    // send와의 차이점은 send는 비동기식(suspend)이고, trySend는 즉시 값을 얻을 수 있는 동기식(일반 함수)입니다.
                    trySend(
                        UiState(
                        editType,
                        if(text.toString().isEmpty())
                            UiState.EditState.EMPTY
                        else
                            UiState.EditState.WRITTEN
                        )
                    )
                }
            }
            //TextWatcher의 해당 리스너를 추가해줍니다.
            //awaitClose에 블록이 실행된 후에 추가 됩니다.
            addTextChangedListener(listener)

            // 콜백이 사라질때 실행, 리스너 제거
            // awaitClose : 채널이 닫히거나 취소되면, 현재의 코루틴을 일시정지하고 재개하기전에 호출됩니다.
            awaitClose { removeTextChangedListener(listener) }
        }.onStart {
            // 학습을 위해 당장은 필요없지만 적어둠
            // onStart 실행 후 -> callbackFlow 실행
            // removeTextChangedListener 리스너가 제거 되기 전까지는 한번만 실행
            Log.d("onStart", "onStart 실행")
        }
    }

}

우선 callbackFlow 블록으로 감싸줍니다.

 

callbackFlow는 callback으로 반환받은 데이터를 flow로 converting 해주는 flow 블록 함수입니다.

 

함수 자체가 내부적으로 channel를 생성하는 ProducerScope를 포함합니다.

 

하나의 flow를 사용할 수 있는 builder라고 보시면 됩니다.

 

내부적으로 보게 되면 buffer는 기본값으로 64개를 사용하게 되는데,

 

onBufferOverflow의 값이 기본값으로

 

BufferOverflow.SUSPEND로 명시되어 있어,

 

channel의 데이터가 64개가 넘어가면,

 

suspending 처리가 되어 send가 block 되게 설정이 되어있습니다.

 

private open class ChannelFlowBuilder<T>(
    private val block: suspend ProducerScope<T>.() -> Unit,
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = BUFFERED,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
) : ChannelFlow<T>(context, capacity, onBufferOverflow) {
    override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow<T> =
        ChannelFlowBuilder(block, context, capacity, onBufferOverflow)

    override suspend fun collectTo(scope: ProducerScope<T>) =
        block(scope)

    override fun toString(): String =
        "block[$block] -> ${super.toString()}"
}

그리고

 

channel scope 범주안에 있기 때문에

 

send와 trySend 함수를 통해 callbackFlow 내부 channel를 통해 값을 송신할 수 있습니다.

 

기존에 channel 함수에서는 값을 송신하길 위해 offer 메서드를 사용했지만,

 

현재 offer 메서드는 Deprecated 되었고, trySend로 대체되었습니다.

 

비슷한 메서드인 send와의 차이점은

 

send는 비동기식(suspend)이고,

 

trySend는 즉시 값을 얻을 수 있는 동기식 메서드입니다.

 

현재 ui상에서 옵저빙 하여 얻은 텍스트 값을 전달하기 때문에,

 

(비동기가 필요 없기 때문에)

 

동기식인 trySend 메서드를 사용하였습니다.

 

text가 빈 값이면 EditState.EMPTY를 넣어주고,

 

아니면 EditState.WRITTEN 값을 넣어주었습니다.

 

//TextWatcher의 해당 리스너를 추가해줍니다.
//awaitClose에 블록이 실행된 후에 추가 됩니다.
addTextChangedListener(listener)

 

이제 이 리스너들을 addTextChangedListener 메서드를 통해 등록해줍니다.

 

만약에 채널이 닫히거나 취소되면 현재의 코 루틴이 일시 정지하게 되는데,

 

이후 다시 코 루틴이 재개를 하게 되면 재개하기 전에

 

removeTextChangedListener가 있는 awaitClose가 호출이 된다.

 

// 콜백이 사라질때 실행, 리스너 제거
// awaitClose : 채널이 닫히거나 취소되면, 현재의 코루틴을 일시정지하고 재개하기전에 호출됩니다.
awaitClose { removeTextChangedListener(listener) }

 

이렇게 제거가 되고,

 

한 번 더 EditText.textChagesToFlow가 호출되면서

 

동시에 addTextChangedListener메서드를 통해 다시 한번 리스너가 등록이 된다.

 

이렇게 일일이 listener를 지워야 하나..라는 의문이 생겨 검색을 해보았다.

 

예전부터 listener를 추가하고, 사용하지 않을 때 굳이 지워야 하나 궁금했기 때문이다.

 

stackoverflow에 상세히 나와 있다.

 

https://stackoverflow.com/questions/48861350/should-i-actually-remove-the-valueeventlistener/48862873

 

Should I actually remove the ValueEventListener?

DatabaseReference Ref = FirebaseDatabase.getInstance().getReference(Constants.Client + "/" + path); Ref.keepSynced(true); Ref.addValueEventListener(new ValueEventListener() { ...

stackoverflow.com

이렇게 리스너를 추가하고, 사용하지 않을 때 제거하지 않는다면,

 

배터리와 대역폭을 낭비하게 된다고 한다.

 

이제 꼭 제거해주자.

 

다음은 

 

onStart부분이다.

 

.onStart {
        // 학습을 위해 당장은 필요없지만 적어둠
        // onStart 실행 후 -> callbackFlow 실행
        // removeTextChangedListener 리스너가 제거 되기 전까지는 한번만 실행
        Log.d("onStart", "onStart 실행")
    }
}

 

이 부분은 당장 필요는 없지만,

 

나중에 쓸 일이 있을 것 같아 학습용으로 두었다.

 

textChangesToFlow 메서드가 호출되면,

 

callbackFlow 블록 안에 코드들이 먼저 실행되는 게 아니라

 

onStart 블록 내부의 함수들이 먼저 호출이 된다.

 

특징이 있다면 리스너가 생성되고 최초 한 번만 실행된다는 것이다.

 

물론 리스너가 제거되고 다시 생성되면 다시 한번 최초 한번 실행이 된다.

 

다음에 유용하게 필요할 때 써먹으면 좋을 것 같다.

 

 

다음 차례는 ViewModel이다. 한번 살펴보자.

 

MainViewModel.kt

/**
 * 알면 좋은 정보
 *
 * emit : 값을 설정하기 위한 호출을 래핑하여 방출하는 suspend 함수이다.
 * 값의 변환이 일어날 때 가장 최근에 보낸 값이 재생 캐시에 저장되어
 * 이전 값과 같을 경우 이전 요소로 그대로 교체해 줍니다.
 * (이전 값이 같으면 값을 방출하지 않는다는 뜻)
 * 쓰레드 세이프 되며 외부 동기화 없이 코루틴 안에서 호출됩니다.
 *
 * value : ex) _name.value
 * 위에 emit과 기능상 별차이가 없다.
 * 차이점은 코루틴 밖에서 호출을 할 수 있다는 점입니다.
 *
 * update : 만약 같은 객체에서 변경사항이 있을 경우 같은 객체에서 변경된 사항만을
 * 업데이트 해주는 함수입니다. 새로운 객체가 아닌 기존 객체에서만 데이터를 변경할 때
 * 사용하기 용이합니다.
 * */
class MainViewModel : ViewModel() {
    private val _name = MutableStateFlow(UiState().copy(type = UiState.EditType.NAME))
    val name: StateFlow<UiState> = _name

    private val _address = MutableStateFlow(UiState().copy(type = UiState.EditType.ADDRESS))
    val address: StateFlow<UiState> = _address

    private val _phoneNum = MutableStateFlow(UiState().copy(type = UiState.EditType.PHONENUM))
    val phoneNum: StateFlow<UiState> = _phoneNum

    // 각 타입의 맞는 데이터를 넣어준다.
    fun emitData(state: UiState?) {
        val editState = state?.editState
        when (state?.type) {
            UiState.EditType.NAME -> updateState(_name, editState)
            UiState.EditType.ADDRESS -> updateState(_address, editState)
            else -> updateState(_phoneNum, editState) // UiState.EditType.PHONENUM
        }
    }

    // 글을 썼는지 안썼는지 비교한 enum 값을 넣어준다.
    private fun updateState(flow: MutableStateFlow<UiState>, state: UiState.EditState?) {
        flow.update {
            if(state == UiState.EditState.WRITTEN) {
                it.copy(editState = UiState.EditState.WRITTEN)
            } else {
                it.copy(editState = UiState.EditState.EMPTY)
            }
        }
    }
}

flow로 변환을 하고 마지막에 livedata로 변환하여 view observing 하는 법도 찾아보았지만,

 

순수 flow로만 작성하는 편이 낫다고 생각이 들었다.

 

private val _name = MutableStateFlow(UiState().copy(type = UiState.EditType.NAME))
val name: StateFlow<UiState> = _name

MutableLiveData와 LiveData 사용법과 비슷하다.

 

각 name, address, phoneNum 값을 설정하였고,

 

copy 메서드를 통해 기본값으로 EditType을 부여했다.

 

// 각 타입의 맞는 데이터를 넣어준다.
fun emitData(state: UiState?) {
    val editState = state?.editState
    when (state?.type) {
        UiState.EditType.NAME -> updateState(_name, editState)
        UiState.EditType.ADDRESS -> updateState(_address, editState)
        else -> updateState(_phoneNum, editState) // UiState.EditType.PHONENUM
    }
}

// 글을 썼는지 안썼는지 비교한 enum 값을 넣어준다.
private fun updateState(flow: MutableStateFlow<UiState>, state: UiState.EditState?) {
    flow.update {
        if(state == UiState.EditState.WRITTEN) {
            it.copy(editState = UiState.EditState.WRITTEN)
        } else {
            it.copy(editState = UiState.EditState.EMPTY)
        }
    }
}

emitData 메서드는 view에서 넘어온 UiState값을 받고,

 

state의 type에 따라 데이터를 업데이트해주는 메서드이다.

 

업데이트를 하는 코드가 지저분한 보일러 플레이트 코드들이어서

 

updateState 메서드로 코드를 빼주었다.

 

flow.update 함수 블록으로 감싸주고, 그 안에는

 

editText가 빈칸인지, 글이 써져있는지에 따라 상태를 갱신하는 코드가 작성되어 있다.

 

여기서 짚고 넘어가면 좋은 정보가 있다.

 

emit vs value vs update

 

flow의 값을 경신하기 위해 3가지 방법이 쓰이는데,

 

각각의 특징과 차이 점을 알아보자.

 

emit

  • 값을 경신하고 래핑 하여 방출되는 suspend 함수이다.
  • 값의 변환이 일어날 때 가장 최근에 보낸 값이 재생 캐시에 저장되어 이전 값과 같을 경우 이전 요소로 그대로 교체해줍니다. (이전 값과 새로운 값이 같으면 값을 방출하지 않는다는 의미입니다. (방출 낭비 X))
  • Thread-Safe가 되어 외부 동기화 없이 코 루틴 안에서 호출할 수 있습니다.

value

  • ex) _name.value = "sooyeol"
  • emit의 기능과 특징은 다를 게 없습니다.
  • 하나 다른 점은 코 루틴 밖에서 호출할 수 있다는 점입니다. (동기식으로 사용 가능)

update

  • 만약 같은 객체에서 변경사항이 있을 경우 같은 객체에서 변경된 프로퍼티만을 업데이트해주는 함수입니다.
  • emit과 value는 기존 객체를 변경해서 방출하는 것이 아닌 새로운 객체를 받고 기존 객체의 프로퍼티와 비교하고 방출할지 말지를 결정하게 되는데, update는 기존 객체에서 일부 프로퍼티 데이터의 변환 여부만을 보고 변경된 기존 객체를 방출해줍니다.

저 같은 경우는 editState의 값만을 WRITTEN or EMPTY로 바꾸면 되기 때문에,

 

update 함수를 사용하였습니다.

 

이제 MainActivity를 보겠습니다.

 

MainActivity.kt

 

import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.example.flow1.R
import com.example.flow1.databinding.ActivityMainBinding
import com.example.flow1.ui.extension.Extensions.textChangesToFlow
import com.example.flow1.ui.widget.UiState
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

class MainActivity : AppCompatActivity() {

    private val vm by viewModels<MainViewModel>()

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        observer()

        lifecycleScope.launch {
            val nameTextFlow = binding.name.textChangesToFlow(UiState.EditType.NAME)
            val addressTextFlow = binding.address.textChangesToFlow(UiState.EditType.ADDRESS)
            val phoneNumTextFlow = binding.phoneNum.textChangesToFlow(UiState.EditType.PHONENUM)

            // merge : 요소의 순서를 유지하지 않고, 다수의 flow를 단일 flow로 병합합니다.
            // 흐름의 수는 제한이 없습니다.
            merge(nameTextFlow, addressTextFlow, phoneNumTextFlow)
                .collect { vm.emitData(it) }

            // 아래 함수를 표현하는 것이 위에 merge 함수이다.
            //flattenMerge : 다수의 flow를 병합하여, 한번에 방출해준다.
            //combine은 확장함수지만, merge(flattenMerge)는 배열의 확장 형태로 되어있습니다.
            // 순서를 유지하지 않는 단일 flow로 만들어 준다는 점이다.
//            flowOf(nameTextFlow, addressTextFlow, phoneNumTextFlow)
//                .flattenMerge()
//                .collect { vm.emitData(it) }
        }
    }

    private fun observer() {
        lifecycleScope.launch(Dispatchers.Main) {
            // collect : 데이터가 들어오는 것과 별개로 수행작업들이 순차적으로 실행이 된다.
            // 모든 값을 수집하는 장점이 있다.
            // 하지만, 여러 데이터 중 중간에 하나의 데이터가 시간을 무제한으로 잡아먹게 되면,
            // 이 후 발행되는 데이터는 모두 처리가 되지 않는다. (앞에 것이 끝날 때까지 기다려야한다.)

            // collectLatest : 최신 데이터가 들어오면 현재 작업을 중지하고 최신 데이터에 따른 수행작업을 우선으로 둔다.
            // 최신 데이터를 이용해 빠르게 UI를 그릴 수 있어서, 더욱 나은 사용자 경험을 줄 수 있습니다.
            // 하지만, 데이터를 발행하는 시간이 빠르고, 수행작업의 시간이 더 오래걸릴 경우 새로 들어온 데이터는
            // 계속해서 취소가 됩니다. 결국 마지막 데이터만 가져오게 되는 상황이 생겨버릴 수 있습니다.

            // 결론: 데이터에 따른 모든 업데이트가 중요하다면 collect를 사용해야하고,
            // 데이터베이스 업데이트 같은 손실 없이 일부 업데이트를 무시해도 되는 작업은 collectLatest를 사용해야 합니다.


            //combine : 다수의 flow 들의 가장 최근 값을 결합하여, flow로 반환합니다.
            //merge랑 다른 점은 이렇게 name, address, phoneNum 처럼 인자를 넘겨 받고 그의 대한
            //각각의 값을 반환할 수 있다는 점이 다른 것 같다.
            combine(vm.name, vm.address, vm.phoneNum) { name, address, phoneNum ->

                val isNameWritten = name.editState == UiState.EditState.WRITTEN
                val isAddressWritten = address.editState == UiState.EditState.WRITTEN
                val isPhoneNumWritten = phoneNum.editState == UiState.EditState.WRITTEN

                //결과 값을 3개의 불리언의 조건식으로 반환합니다.
                //현재 EditText가 모두 타이핑이 되어 있는지 확인하고 값을 넘겨줍니다.
                isNameWritten && isAddressWritten && isPhoneNumWritten

            }.collect {
                //만약 collect로 넘어온 값이 true이면 버튼의 색상을 활성화 합니다.
                binding.button.apply {
                    text = if(it) {
                        setBackgroundResource(R.color.purple_700)
                        "활성화"
                    } else {
                        setBackgroundResource(R.color.grey)
                        "비활성화"
                    }
                }
            }
        }
    }
}

 

편리한 예제 코드를 위해서 viewBinding을 사용했습니다.

 

우선 아래 코드부터 보겠습니다.

 

lifecycleScope.launch {
            val nameTextFlow = binding.name.textChangesToFlow(UiState.EditType.NAME)
            val addressTextFlow = binding.address.textChangesToFlow(UiState.EditType.ADDRESS)
            val phoneNumTextFlow = binding.phoneNum.textChangesToFlow(UiState.EditType.PHONENUM)

            // merge : 요소의 순서를 유지하지 않고, 다수의 flow를 단일 flow로 병합합니다.
            // 흐름의 수는 제한이 없습니다.
            merge(nameTextFlow, addressTextFlow, phoneNumTextFlow)
                .collect { vm.emitData(it) }

            // 아래 함수를 표현하는 것이 위에 merge 함수이다.
            //flattenMerge : 다수의 flow를 병합하여, 한번에 방출해준다.
            //combine은 확장함수지만, merge(flattenMerge)는 배열의 확장 형태로 되어있습니다.
            // 순서를 유지하지 않는 단일 flow로 만들어 준다는 점이다.
//            flowOf(nameTextFlow, addressTextFlow, phoneNumTextFlow)
//                .flattenMerge()
//                .collect { vm.emitData(it) }
        }

lifecycleScope 범주에 둠으로써 suspend 메서드를 쓸 준비를 해줍니다.

 

아까 Extensions에 두었던 EditText 확장 함수인 textChangesToFlow 메서드를 여기서 써줍니다.

 

각각에 맞는 UiState type을 넣어주고, 그에 맞는 flow를 반환받습니다.

 

그 후 반환받은 저 세 flow를 하나로 병합합니다.

 

병합할 때 쓰는 메서드는 merge 메서드입니다.

 

merge 메서드는 파라미터로 전달받은 플로우의 방출된 값들을 받고,

 

그 값들의 순서를 유지하지 않고, 단일 flow로 병합하여 전달해줍니다.

 

한 가지 더 알아야 할 점은 flow의 수는 제한이 없다는 점이다.

 

이 merge 메서드를 아래 함수로도 표현할 수 있다.

flowOf(nameTextFlow, addressTextFlow, phoneNumTextFlow)
    .flattenMerge()
    .collect { vm.emitData(it) }

flattenMerge는 다수의 flow를 병합하여, 한 번에 방출해주는 메서드입니다.

 

merge메서드와 같은 역할을 하고 있습니다.

 

이 역시 순서를 유지하지 않는 단일 flow로 만들어 줍니다.

 

이제 아래에서 나올 코드에서 사용되는 combine메서드와 비슷하지만,

 

combine은 확장 함수, merge(falttenMerge)는 배열의 확장 형태로 되어있습니다.

 

게다가 combine은 다수의 flow들을 병합하여 가장 최근 값을 flow로 반환하고,

 

병합한 flow들의 각각의 인자 값을 넘겨받아 활용할 수 있는 유용한 점이 추가로 더 있습니다.

 

이제 다음은 ui를 변경하는 부분을 보겠습니다.

 

private fun observer() {
        lifecycleScope.launch(Dispatchers.Main) {
            // collect : 데이터가 들어오는 것과 별개로 수행작업들이 순차적으로 실행이 된다.
            // 모든 값을 수집하는 장점이 있다.
            // 하지만, 여러 데이터 중 중간에 하나의 데이터가 시간을 무제한으로 잡아먹게 되면,
            // 이 후 발행되는 데이터는 모두 처리가 되지 않는다. (앞에 것이 끝날 때까지 기다려야한다.)

            // collectLatest : 최신 데이터가 들어오면 현재 작업을 중지하고 최신 데이터에 따른 수행작업을 우선으로 둔다.
            // 최신 데이터를 이용해 빠르게 UI를 그릴 수 있어서, 더욱 나은 사용자 경험을 줄 수 있습니다.
            // 하지만, 데이터를 발행하는 시간이 빠르고, 수행작업의 시간이 더 오래걸릴 경우 새로 들어온 데이터는
            // 계속해서 취소가 됩니다. 결국 마지막 데이터만 가져오게 되는 상황이 생겨버릴 수 있습니다.

            // 결론: 데이터에 따른 모든 업데이트가 중요하다면 collect를 사용해야하고,
            // 데이터베이스 업데이트 같은 손실 없이 일부 업데이트를 무시해도 되는 작업은 collectLatest를 사용해야 합니다.


            //combine : 다수의 flow 들의 가장 최근 값을 결합하여, flow로 반환합니다.
            //merge랑 다른 점은 이렇게 name, address, phoneNum 처럼 인자를 넘겨 받고 그의 대한
            //반환을 할 수 있는 인터페이스를 제공한다는 점, 순서를 유지하지 않는 단일 flow로 만들어 준다는 점이다.
            combine(vm.name, vm.address, vm.phoneNum) { name, address, phoneNum ->

                val isNameWritten = name.editState == UiState.EditState.WRITTEN
                val isAddressWritten = address.editState == UiState.EditState.WRITTEN
                val isPhoneNumWritten = phoneNum.editState == UiState.EditState.WRITTEN

                //결과 값을 3개의 불리언의 조건식으로 반환합니다.
                //현재 EditText가 모두 타이핑이 되어 있는지 확인하고 값을 넘겨줍니다.
                isNameWritten && isAddressWritten && isPhoneNumWritten

            }.collect {
                //만약 collect로 넘어온 값이 true이면 버튼의 색상을 활성화 합니다.
                binding.button.apply {
                    text = if(it) {
                        setBackgroundResource(R.color.purple_700)
                        "활성화"
                    } else {
                        setBackgroundResource(R.color.grey)
                        "비활성화"
                    }
                }
            }

ui를 업데이트해야 하기 때문에 메인 스레드에서 진행이 필요했고,

 

Dispatchers를 이용해 scope에서 실행을 메인 스레드로 변경하였습니다.

 

여기서 collect와 collectLatest에 대해서 알고 가보겠습니다.

 

flow를 통해 방출된 값을 수집하는 곳이 collect입니다.

 

이 두 가지 종류가 있는데 각각의 특징을 알아보겠습니다.

 

collect

  • 데이터가 들어오는 것과 별개로 수행 작업들이 순차적으로 실행이 된다
  • 모든 값을 수집하는 장점이 있다.
  • 하지만, 여러 데이터 중 중간에 하나의 데이터가 시간을 무제한으로 잡아먹게 되면,
  • 이후 발행되는 데이터는 모두 처리가 되지 않는다. (앞에 수행 작업이 끝날 때까지 기다리느라 앞에 것을 해결하지 못함.)

 

collectLatest

  • 최신 데이터가 들어오면 현재 작업을 중지하고, 최신 데이터에 따른 수행 작업을 우선으로 둔다.
  • 최신 데이터를 이용해 빠르게 UI를 그릴 수 있어서, 더 나은 사용자 경험을 줄 수 있다.
  • 하지만, 데이터를 발행하는 시간이 빠르고, 수행 작업의 시간이 더 오래 걸릴 경우 새로 들어온 데이터는 계속해서 취소가 된다.
  • 결국 마지막 데이터만 가져오게 되는 상황이 생겨 버릴 수도 있습니다.

결론 : 데이터에 따른 모든 업데이트를 중요시한다면, collect를 사용해야 하고,

데이터베이스 업데이트 같은 손실 없이 일부 업데이트를 무시해도 되는 작업은

collectLatest를 사용해야 한다.

 

두 메서드의 특징을 살펴보고 적합한 것을 사용하면 될 것 같다.

 

그다음은 combine이다.

 

combine(vm.name, vm.address, vm.phoneNum) { name, address, phoneNum ->

                val isNameWritten = name.editState == UiState.EditState.WRITTEN
                val isAddressWritten = address.editState == UiState.EditState.WRITTEN
                val isPhoneNumWritten = phoneNum.editState == UiState.EditState.WRITTEN

                //결과 값을 3개의 불리언의 조건식으로 반환합니다.
                //현재 EditText가 모두 타이핑이 되어 있는지 확인하고 값을 넘겨줍니다.
                isNameWritten && isAddressWritten && isPhoneNumWritten

            }

combine : 다수의 flow들의 가장 최근 값을 결합하여, flow로 반환다.

 

merge와 같이 순서를 유지하지 않고 단일 flow로 만들어준다.

 

하지만, name, address, phoneNum처럼 인자를 넘겨받는

 

인터페이스를 제공받는다는 점이 다르다.

 

마지막 줄에 세 개의 불리언 값에 따른 값을 반환하여, collect로 넘겨준다.

 

.collect {
                //만약 collect로 넘어온 값이 true이면 버튼의 색상을 활성화 합니다.
                binding.button.apply {
                    text = if(it) {
                        setBackgroundResource(R.color.purple_700)
                        "활성화"
                    } else {
                        setBackgroundResource(R.color.grey)
                        "비활성화"
                    }
                }
            }

만약 3개의 불리언이 true이면 collect에서 true값이 전달되어,

 

버튼의 색깔을 보라색으로 바꾸고, 텍스트도 활성화로 바꾼다.

 

반대 상황도 똑같다.

 

이렇게 필수 값에 따른 ui변경에 대한 부분을 flow로 다루어 보았다.

 

위에서 다른 코드는 아래 깃허브에 정리해두었다.

 

https://github.com/qjsqjsaos/FlowUses

 

GitHub - qjsqjsaos/FlowUses: 후맛집 프로젝트 -> Flow 활용 과제

후맛집 프로젝트 -> Flow 활용 과제. Contribute to qjsqjsaos/FlowUses development by creating an account on GitHub.

github.com

 

728x90
반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading