한 애플리케이션에서 여러 플랫폼의 웹툰을 볼 수 있는 프로젝트를 개발하던 중 발생한 일입니다.
웹뷰를 이용하여 네이버 웹툰처럼 요일 별, 웹툰을 클릭했을 때 웹뷰를 통해 그 화면으로 넘어갑니다.
fun moveToWeb(work: Work){
val urlMap = mapOf(
"네이버" to "https://m.comic.naver.com/search/result?keyword=${work.title}",
"카카오페이지" to "https://page.kakao.com/search/result?keyword=${work.title}",
"리디" to "https://ridibooks.com/search?q=${work.title}&adult_exclude=n",
"레진코믹스" to "https://www.lezhin.com/ko/search?t=all&q=${work.title}",
"봄툰" to "https://www.bomtoon.com/search?q=${work.title}",
"기타" to work.url
)
val intent = Intent(context, ViewerActivity::class.java)
intent.putExtra("url", urlMap[work.platform])
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(context, intent, null)
}
플랫폼에 따라 미리 설정해둔 url을 통해 웹툰을 보여줍니다.
화면의 하단을 보면 <-, <, > 버튼이 있는 것을 알 수 있습니다.
이는 웹뷰에서 웹뷰 나가기, 이전 화면 이동, 다음 화면 이동 기능을 제공하기 위한 버튼입니다.
웹뷰의 이전 화면으로 가기 위해 안드로이드의 뒤로 가기 버튼을 누르면 웹뷰에서 나가지기 때문에 이 부분을 보완하기 위해 버튼을 만들었습니다.
이 버튼을 만들기 위해 LaunchedEffect를 사용하였다.
: composable 함수에서 side effect를 처리하기 위한 도구
(side effect - composable 함수의 리컴포지션과 별개로 실행되는 코드. 비동기 작업이나 상태 변화와 같은 작업을 수행할 때 사용)
LaunchedEffect는 매개변수로 key 값을 가집니다. 그리고 key 중 하나가 변경될 때마다 블록 내의 동작을 취소하고, 다시 실행합니다.
저는 LaunchedEffect의 key 값으로 Unit을 넣어주었습니다.
LaunchedEffect(Unit) {
viewModel.undoSharedFlow.collectLatest {
if (webView.canGoBack()) {
webView.goBack()
} else {
Toast.makeText(context, "뒤로 갈 수 없습니다.", Toast.LENGTH_SHORT).show()
}
}
}
package kotlin
/**
* The type with only one value: the `Unit` object. This type corresponds to the `void` type in Java.
*/
public object Unit {
override fun toString(): String = "kotlin.Unit"
}
Unit의 코드를 확인하면 다음과 같습니다.
주석에는 java의 void와 대응되는 타입으로, 하나의 값만 가진다고 설명하고 있습니다.
https://medium.com/@sandeepkella23/everything-about-unit-in-kotlin-e40a01829098
링크의 게시글에서 그 이유를 찾을 수 있었습니다.
이러한 차이 때문에 kotlin에서는 변수에 expression (식)을 자유롭게 넣을 수 있고, 이를 매개변수로 받을 수 있습니다.
다시 LaunchedEffect 이야기로 돌아와서, LaunchedEffect의 key 값을 Unit으로 설정하면 컴포저블 수명 주기 동안 1번만 side effect을 트리거할 수 있습니다.
따라서 컴포저블의 생명 주기는 다음과 같이 설명할 수 있습니다.
웹뷰에서 이전 화면으로 돌아가고, 다음 화면으로 넘어가는 것은 화면은 바뀔지라도 UI 구성에서 변화가 생기지 않습니다.
또한 저는 Flow를 이용하여 명령을 수집할 것이기 때문에 Flow를 collect하도록 하는 함수를 한 번만 실행시키면 됩니다.
: 코틀린의 코루틴 라이브러리에서 제공하는 비동기 데이터 스트림 처리를 위한 API
알람이 왔다는 사실을 알리기 위해 MutableStateFlow를 사용하는 것은 적절하지 않습니다.
(MutableStateFlow - stateFlow의 변형. 읽기와 쓰기 모두 가능)
왜냐하면 MutableStateFlow은 같은 값을 연속해서 보내면 구독자가 이를 인식하지 않기 때문입니다. 이를 사용하게 되면 webView가 실행되고 딱 1번만 MutableStateFlow 값을 인식하게 됩니다.
따라서 우리는 sharedFlow를 사용해야 합니다.
: 여러 수집기에게 동일한 데이터를 전송할 수 있는 데이터 스트림
마찬가지로 MutableSharedFlow는 SharedFlow의 변형으로 데이터 일기와 쓰기 모두 가능합니다.
MutableSharedFlow는 동일한 값을 발행하여도 구독자가 모두 수신하기 때문에 이벤트 처리에 더 적합합니다.
따라서 ViewModel에선 아래와 같이 코드를 작성하였습니다.
private val _undoSharedFlow = MutableSharedFlow<Unit>()
val undoSharedFlow = _undoSharedFlow.asSharedFlow()
private val _redoSharedFlow = MutableSharedFlow<Unit>()
val redoSharedFlow = _redoSharedFlow.asSharedFlow()
fun undo() {
viewModelScope.launch {
_undoSharedFlow.emit(Unit)
}
}
fun redo() {
viewModelScope.launch {
_redoSharedFlow.emit(Unit)
}
}
이젠 이전 화면, 다음 화면으로 잘 이동할 수 있습니다.😁
이벤트가 발생할 때마다 값을 바꿔 입력하기는 번거로우니까, 그냥 MutableSharedFlow를 사용하자!