본문 바로가기

만났던 에러들

java.lang.IllegalStateException: Flow invariant is violated:Emission from another coroutine is detected.

에러 내용

java.lang.IllegalStateException: Flow invariant is violated:Emission from another coroutine is detected.
Child of StandaloneCoroutine{Active}@b93bc05, expected child of StandaloneCoroutine{Completed}@20ec45a.
FlowCollector is not thread-safe and concurrent emissions are prohibited.
                                                                                                     To mitigate this restriction please use 'channelFlow' builder instead of 'flow'

 

아마 글자가 작아서 잘 안보이실분들을 위해서 사진으로도 남기겠습니다.

 

에러가 발생한 이유는 코루틴 영역안에서 다시한번 코루틴 영역을 호출하려고 시도했기 때문입니다.

 

에러를 꼼꼼히 읽어보면 정말 좋은게 원인도 알 수 있고 해결법도 알 수 있습니다.

에러의 문장 마지막 부근을 보면 아래와 같은 내용이 있네요.

 

To mitigate this restriction please use 'channelFlow' builder instead of 'flow'

 

flow 대신 channelFlow를 쓰는 것을 권장한다고 합니다.

우선 channelFlow가 무엇인지 살펴보기전에 제 코드가 어땠길래 이런 에러가 발생했는지 한번 짧게 살펴보시죠.

 

LoginActivity.kt

@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
    private val loginActivityViewModel: LoginActivityViewModel by viewModels()


    private var resultLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == RESULT_OK) {
                val data: Intent? = result.data
                val task: Task<GoogleSignInAccount> =
                    GoogleSignIn.getSignedInAccountFromIntent(data)
                // 코루틴을 시작한 부분
                lifecycleScope.launch {
                    loginActivityViewModel.signInWithGoogle(task)
                }
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        Logger.addLogAdapter(AndroidLogAdapter())
        val binding = ActivityLoginBinding.inflate(layoutInflater)
        setContentView(binding.root)

		...
    }

}

 

구글 로그인 기능을 구현하고 있었습니다. 로그인 화면에서 구글로 로그인 버튼을 누르고 나면 registerActivityForResult를 통해서 구글로그인에 대한 응답값을 받아오죠. 여기서 저희가 살펴봐야할 코드는 아래 코드입니다.

lifecycleScope.launch {
loginActivityViewModel.signInWithGoogle(task)
}

 

액티비티에서 한번 코루틴 스코프를 시작했습니다. 그리고 뷰모델 -> useCase로 넘어갑니다.

 

LoginActivityUseCase.kt

class LoginActivityUseCase() {

    fun signInWithGoogle(completedTask: Task<GoogleSignInAccount>) = flow{
        try {

            val account = completedTask.getResult(ApiException::class.java)
            val photoUrl = account.photoUrl.toString()
            Logger.v(photoUrl)

            val mAuth = FirebaseAuth.auth

            val credential: AuthCredential =
                GoogleAuthProvider.getCredential(account.getIdToken(), null);
            mAuth.signInWithCredential(credential)
                .addOnCompleteListener {
                    if (it.isSuccessful) {
                        GlobalScope.launch {
                            emit(it.isSuccessful)
                        }

                    }
                }

        } catch (e: ApiException) {
            Logger.v(e.message.toString())
            Logger.v(e.statusCode.toString())

        }

    }
}

 

코드가 조금 길긴한데 다 필요없고 GlobalScope.launch 부분만 보시면 됩니다. 이미 이전에 activity에서 코루틴 스코프가 시작됐는데 제가 여기서 한번 또 시작시켰습니다. 중첩되서 글의 제목에서 말씀드렸던 에러가 발생한거죠.

 

channelFlow를 사용하면 이 문제를 간단하게 해결할 수 있습니다.

channelFlow를 사용하면 여러개의 context를 사용하는게 가능합니다.

아래 코드는 해결됐을때의 코드입니다.

 

LoginACtivity.kt

@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
    private val loginActivityViewModel: LoginActivityViewModel by viewModels()


    private var resultLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == RESULT_OK) {
                val data: Intent? = result.data
                val task: Task<GoogleSignInAccount> =
                    GoogleSignIn.getSignedInAccountFromIntent(data)
                lifecycleScope.launch {
                    loginActivityViewModel.signInWithGoogle(task)
                }
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        Logger.addLogAdapter(AndroidLogAdapter())
        val binding = ActivityLoginBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ...     
}

 

LoginActivityUseCase.kt

class LoginActivityUseCase() {

    fun signInWithGoogle(completedTask: Task<GoogleSignInAccount>) = callbackFlow{
        try {

            val account = completedTask.getResult(ApiException::class.java)
            val photoUrl = account.photoUrl.toString()
            Logger.v(photoUrl)

            val mAuth = FirebaseAuth.auth

            val credential: AuthCredential =
                GoogleAuthProvider.getCredential(account.getIdToken(), null);
            mAuth.signInWithCredential(credential)
                .addOnCompleteListener {
                    if (it.isSuccessful) {
                        GlobalScope.launch {
                            trySend(it.isSuccessful)


                        }

                    }
                }

        } catch (e: ApiException) {
            Logger.v(e.message.toString())
            Logger.v(e.statusCode.toString())

        }
        awaitClose()
    }
}

 

오늘도 즐코딩하세요