Shunz Android Dev Note
안드로이드 - 네트워크 연결 상태 체크 (feat. Compose, Flow) 본문
이번 시간에는 네트워크 연결 상태를 실시간으로 옵저빙 할 수 있는 방법에 대하여 포스팅을 해 보겠습니다. Compose 기반으로 작성되었지만, Compose가 아닌 기존의 UI 개발 방식(명령형)에서도 사용을 할 수 있습니다.
솔루션1
ConnectivityObserver 라는 인터페이스를 생성합니다. 네트워크 연결 상태를 실시간 받기 위하여 Flow 형태로 응답을 받도록 메서드를 하나 선언합니다. 상태는 총 4가지를 가지고 있습니다.
interface ConnectivityObserver {
fun getFlow(): Flow<Status>
enum class Status {
Available, Unavailable, Losing, Lost
}
}
인터페이스를 구현한 구체 클래스를 생성합니다. getFlow()의 내부를 보면 callbackFlow를 통하여 Status를 Flow로 내보내고 있습니다. 먼저, NetworkCallback 인터페이스를 구현해주고, 이 콜백을 connectivityManager에 등록을 합니다. callbackFlow의 마지막 연산자로 distinctUntilChanged는 동일한 상태값이 방출 되는 경우 무시하고, 상태값이 변경된 경우에만 구독자에게 전달한다는것을 의미합니다.
한가지 중요한 점은, awaitClose에서 더 이상 사용을 하지 않을 때에는 등록을 해제합니다. 그렇지 않으면 콜백이 해제되지 않아서 메모리 릭이 발생 할 수 있습니다.
class NetworkConnectivityObserver(
context: Context,
) : ConnectivityObserver {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
override fun getFlow(): Flow<ConnectivityObserver.Status> {
return callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
launch { send(ConnectivityObserver.Status.Available) }
}
override fun onLosing(network: Network, maxMsToLive: Int) {
super.onLosing(network, maxMsToLive)
launch { send(ConnectivityObserver.Status.Losing) }
}
override fun onLost(network: Network) {
super.onLost(network)
launch { send(ConnectivityObserver.Status.Lost) }
}
override fun onUnavailable() {
super.onUnavailable()
launch { send(ConnectivityObserver.Status.Unavailable) }
}
}
connectivityManager.registerDefaultNetworkCallback(callback)
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
}
}.distinctUntilChanged()
}
}
어떻게 사용 할 수 있을까요? 컴포저블에서 사용하는 간단한 예제 코드입니다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StableImmutableTheme {
Column(
Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
NetworkStatusLogView(NetworkConnectivityObserver(applicationContext).getFlow())
}
}
}
}
}
@Composable
fun NetworkStatusLogView(statusFlow: Flow<ConnectivityObserver.Status>) {
val status: ConnectivityObserver.Status by statusFlow.collectAsStateWithLifecycle(ConnectivityObserver.Status.Unavailable)
Box(
contentAlignment = Alignment.Center
) {
Text(text = "네트워크 상태 : $status")
}
}
참고로, collectAsStateWithLifecycle 메서드는 다음 의존 라이브러리를 추가하여 사용 할 수 있습니다.
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2"
다음 영상을 통하여 네트워크 상태에 따른 텍스트의 렌더링을 확인 할 수 있습니다.
문제점
위에 설명한 코드에 작은 문제점이 있습니다. 무엇일까요?
getFlow()를 호출 할 때마다, 콜백 객체를 생성하고 이를 매번 connectivityManager에 등록하는 과정을 거치게 됩니다. 또한, Cold Stream이기 때문에 구독하기 전까지는 상태를 알수가 없다는 단점도 있습니다.
이런 이슈들을 해소 하려면 언제 어느 시점에 getFlow를 호출 하더라도 NetworkCallback의 구체 클래스는 단 한번만 생성을 해야 합니다. 또한 Cold Stream이 아닌 Hot Stream으로 구독자는 상태 값을 바로 알 수 있어야 합니다.
솔루션2
다음 코드를 보겠습니다. 인터페이스는 그대로 두되, 구체 클래스만 수정해보겠습니다.
설명은 코드 다음에 이어서 하겠습니다.
class NetworkConnectivityObserverSingleton(
context: Context,
) : ConnectivityObserver {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val status: Flow<ConnectivityObserver.Status> =
observe().stateIn(GlobalScope, WhileSubscribed(), ConnectivityObserver.Status.Unavailable)
override fun getFlow(): Flow<ConnectivityObserver.Status> {
return status
}
private fun observe(): Flow<ConnectivityObserver.Status> {
return callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
launch { send(ConnectivityObserver.Status.Available) }
}
override fun onLosing(network: Network, maxMsToLive: Int) {
super.onLosing(network, maxMsToLive)
launch { send(ConnectivityObserver.Status.Losing) }
}
override fun onLost(network: Network) {
super.onLost(network)
launch { send(ConnectivityObserver.Status.Lost) }
}
override fun onUnavailable() {
super.onUnavailable()
launch { send(ConnectivityObserver.Status.Unavailable) }
}
}
connectivityManager.registerDefaultNetworkCallback(callback)
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
}
}.distinctUntilChanged()
}
companion object {
@Volatile
private var instance: ConnectivityObserver? = null
fun getInstance(context: Context): ConnectivityObserver {
return instance ?: synchronized(this) {
instance ?: NetworkConnectivityObserverSingleton(context).also {
instance = it
}
}
}
}
}
어떤 부분들이 변경되었을까요? 가장 큰 변경점은 구체 클래스 자체가 싱글턴이라는 점입니다. getInstance를 통하여 싱글턴 객체로 제공을 하고 있습니다.
또한, 멤버 변수로 status 를 가지고 있다는 점입니다. 이 변수는 단 한번만 내부 메서드인 getFlow()를 통하여 콜백 인스턴스를 생성 후 connectivityManager에 등록을 하고 있습니다. stateIn()을 통하여 가지고 있던 상태값은 여러 구독자(+신규 구독자들)에게 최신의 상태값을 바로 전달 할 수 있습니다. 이를 통하여 Hot Stream이 가능하게 됩니다. 코루틴 스코프는 앱의 라이프 사이클에 맞추기 위하여 GlobalScope로 지정하였습니다. 물론, 스코프를 커스텀하여 특정 상태에서만 옵저빙 하는것도 가능합니다.
Caller 코드는 크게 바뀐것이 없습니다. 싱글턴 객체를 통하여 getFlow()를 호출합니다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StableImmutableTheme {
Column(
Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
val context = applicationContext
NetworkStatusLogView(
NetworkConnectivityObserverSingleton.getInstance(context).getFlow()
)
NetworkStatusLogView(
NetworkConnectivityObserverSingleton.getInstance(context).getFlow()
)
}
}
}
}
}
@Composable
fun NetworkStatusLogView(statusFlow: Flow<ConnectivityObserver.Status>) {
val status: ConnectivityObserver.Status by
statusFlow.collectAsStateWithLifecycle(ConnectivityObserver.Status.Unavailable)
Box(
contentAlignment = Alignment.Center
) {
Text(text = "네트워크 상태 : $status")
}
}
데모 영상을 보겠습니다. 기대한대로 잘 동작하는것을 확인 할 수 있습니다.
마치며...
네트워크 연결 상태를 체크하는 모던한 방법을 소개 하였습니다. ViewModel에서도 StateFlow 등을 멤버로 하여 사용할 수도 있고, Compose에서도 위와 같은 방식으로 사용을 할 수 있습니다. 더 괜찮은 방법을 알고 계시다면 코멘트 남겨 주세요. 참고해서 추가 업데이트 하겠습니다. :-)
'Android' 카테고리의 다른 글
안드로이드 LiveData와 Flow는 각각 언제 사용해야 할까? (0) | 2023.11.29 |
---|