안드로이드 compose + Flow + connectivityManager를 활용한 네트워크 연결 관리

이우건·2024년 12월 4일
0

안드로이드

목록 보기
19/20

현재 대부분 배포되어 있는 앱들은 네트워크 연결이 끊겼을 때 (Wifi나 모바일 데이터가 끊겼을 때) 사용자에게 시각적으로 네트워크 연결이 끊겼음을 알려주는 ui를 보여줍니다.

이 포스팅은 https://medium.com/@meytataliti/obtaining-network-connection-info-with-flow-in-android-af2e6b760dfd 를 참고하여 작성되었으며 connectivityManager를 활용해 네트워크 연결 상태를 가져오고 이를 flow로 변환해 Ui에 보여주는 과정을 보여줍니다.

권한

네트워크 사용을 위해선 안드로이드 매니페스트에 다음 권한을 포함해야 합니다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

DI

@Module
@InstallIn(SingletonComponent::class)
object NetworkConnectionModule {

    @Provides
    @Singleton
    fun provideNetworkManager(
        @ApplicationContext context: Context
    ): NetworkManager = NetworkManager(context)
}

ConnectivityManager 객체를 생성하기 위해서는 context가 필요합니다. 또한 네트워크 관리 객체는 한 번만 생성되면되므로 Hilt를 이용하여 앱 전체에서 싱글톤으로 의존성으로 제공합니다.

NetworkManager

class NetworkManager @Inject constructor(
    context: Context
) {
    private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    private var isNetworkCallbackRegistered = false

    private val isConnected: Boolean
        @SuppressLint("MissingPermission")
        get() {
            val activeNetwork = connectivityManager.activeNetwork
            return if (activeNetwork == null) {
                false
            } else {
                val netCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
                (netCapabilities != null
                        && netCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                        && netCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
            }
        }
}
...

먼저 ConnectivityManager의 객체를 생성하고 사용자의 네트워크 연결 상태(isConnected)를 확인합니다.
인터넷 연결이 유효(NET_CAPABILITY_INTERNET 및 NET_CAPABILITY_VALIDATED)한 경우 true를 반환합니다.

하지만 인터넷 연결성은 언제든지 변경될 수 있습니다. ConnectivityManager.NetworkCallback을 활용하여 변경 사항을 수신 할 수 있습니다.

private fun connectionFlow() = callbackFlow {
        val networkCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onLost(network: Network) {
                trySend(false)
            }

            override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
                if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
                    networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
                    trySend(true)
                }
            }
        }

        subscribe(networkCallback)
        awaitClose { unsubscribe(networkCallback) }
    }

    @SuppressLint("MissingPermission")
    fun subscribe(networkCallback: ConnectivityManager.NetworkCallback) {
        connectivityManager.registerDefaultNetworkCallback(networkCallback)
        isNetworkCallbackRegistered = true
    }

    private fun unsubscribe(networkCallback: ConnectivityManager.NetworkCallback) {
        if (isNetworkCallbackRegistered) {
            try {
                connectivityManager.unregisterNetworkCallback(networkCallback)
                isNetworkCallbackRegistered = false
            } catch (e: IllegalArgumentException) {
                e.printStackTrace()
            }
        }
    }

callbackFlow를 사용해 ConnectivityManager의 네트워크 상태 콜백을 비동기로 관찰합니다.
networkCallback을 정의하여 ConnectivityManager에 등록합니다.
Flow가 수집을 중단하거나 취소하면 awaitClose로 등록을 해제하고 리소스를 정리합니다.

NetworkCallback

  • onLost : 네트워크 연결이 끊기면 trySend 함수를 통해 채널에 false를 추가하여 callbackFlow가 false를 방출하도록 합니다.
  • onCapabilitiesChanged : 네트워크의 특성(NetworkCapabilities)이 변경되고 인터넷 연결이 유효한다면 trySend 함수를 통해 채널에 true를 추가하여 callbackFlow가 true를 방출하도록 합니다.

registerDefaultNetworkCallback을 통해 NetworkCallback 등록은 앱 당 최대 100개로 제한 됩니다. 만약 제한을 초과하면 예외(Exception)가 발생합니다.
이 문제를 방지하기 위해 더 이상 필요하지 않은 네트워크 콜백은 반드시 unregisterNetworkCallback(ConnectivityManager.NetworkCallback)을 사용해 등록 해제를 해야 합니다.

registerDefaultNetworkCallback는 Api 24에 추가 되었습니다. 만약 min sdk를 24보다 밑이라면 registerNetworkCallback로 네트워크 콜백을 등록해야 합니다. (registerNetworkCallback는 Api 21)

StateFlow

fun connectionStateFlow(externalScope: CoroutineScope): StateFlow<Boolean> {
        return connectionFlow()
            .catch { e -> e.printStackTrace() }
            .stateIn(
                scope = externalScope,
                started = SharingStarted.Lazily,
                initialValue = isConnected
            )
    }

위에서 선언한 Flow를 핫 플로우로 변환하여 Ui Layer에서 구독하기 위해 StateIn() 사용하여 StateFlow로 변환합니다.

전체 코드

class NetworkManager @Inject constructor(
    context: Context
) {
    private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    private var isNetworkCallbackRegistered = false

    private val isConnected: Boolean
        @SuppressLint("MissingPermission")
        get() {
            val activeNetwork = connectivityManager.activeNetwork
            return if (activeNetwork == null) {
                false
            } else {
                val netCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
                (netCapabilities != null
                        && netCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                        && netCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
            }
        }

    fun connectionStateFlow(externalScope: CoroutineScope): StateFlow<Boolean> {
        return connectionFlow()
            .catch { e -> e.printStackTrace() }
            .stateIn(
                scope = externalScope,
                started = SharingStarted.Lazily,
                initialValue = isConnected
            )
    }

    private fun connectionFlow() = callbackFlow {
        val networkCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onLost(network: Network) {
                trySend(false)
            }

            override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
                if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
                    networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
                    trySend(true)
                }
            }
        }

        subscribe(networkCallback)
        awaitClose { unsubscribe(networkCallback) }
    }

    @SuppressLint("MissingPermission")
    fun subscribe(networkCallback: ConnectivityManager.NetworkCallback) {
        connectivityManager.registerDefaultNetworkCallback(networkCallback)
        isNetworkCallbackRegistered = true
    }

    private fun unsubscribe(networkCallback: ConnectivityManager.NetworkCallback) {
        if (isNetworkCallbackRegistered) {
            try {
                connectivityManager.unregisterNetworkCallback(networkCallback)
                isNetworkCallbackRegistered = false
            } catch (e: IllegalArgumentException) {
                e.printStackTrace()
            }
        }
    }
}

UI

만약 멀티 모듈 구조로 안드로이드를 개발하고 있고 모든 화면에 네트워크가 끊겼을 시 네트워크 연결 관련 안내 Ui를 보여주고 싶다면 MainActivity에서 Flow를 구독합니다.

@AndroidEntryPoint
class MainActivity: ComponentActivity() {

    @Inject
    lateinit var networkManager: NetworkManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val networkState by networkManager.connectionStateFlow(lifecycleScope).collectAsStateWithLifecycle()

            NetworkConnectionBox(
                modifier = Modifier
                    .fillMaxWidth(),
                networkState = networkState
            )
        }
    }
}

NetworkConnectionBox 는 다음과 같이 구현되어 있습니다.

@Composable
fun NetworkConnectionBox(
    networkState: Boolean,
    modifier: Modifier = Modifier
) {
    TopToBottomToTopAnimatedVisibility(
        visible = !networkState
    ) {
        Surface(
            modifier = modifier
                .padding(10.dp),
            color = Gray4.copy(alpha = 0.5f),
        ) {
            Box(
                contentAlignment = Alignment.Center
            ) {
                Text(
                    modifier = Modifier
                        .padding(vertical = 5.dp),
                    text = stringResource(id = R.string.error_message_network)
                )
            }
        }
    }
}

@Composable
fun TopToBottomToTopAnimatedVisibility(
    visible: Boolean,
    content: @Composable AnimatedVisibilityScope.() -> Unit
) {
    AnimatedVisibility(
        visible = visible,
        enter = slideInVertically(
            initialOffsetY = { fullHeight -> -fullHeight },
            animationSpec = tween(300)
        ),
        exit = slideOutVertically(
            targetOffsetY = { fullHeight -> -fullHeight },
            animationSpec = tween(300)
        ),
        content = content
    )
}

networkState가 false면 AnimatedVisibility 를 통해 네트워크 에러 메시지를 보여주는 Box를 애니메이션으로 보여줍니다.

결과


참조: https://medium.com/@meytataliti/obtaining-network-connection-info-with-flow-in-android-af2e6b760dfd

profile
머리가 나쁘면 기록이라도 잘하자

0개의 댓글