현재 대부분 배포되어 있는 앱들은 네트워크 연결이 끊겼을 때 (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" />
@Module
@InstallIn(SingletonComponent::class)
object NetworkConnectionModule {
@Provides
@Singleton
fun provideNetworkManager(
@ApplicationContext context: Context
): NetworkManager = NetworkManager(context)
}
ConnectivityManager 객체를 생성하기 위해서는 context가 필요합니다. 또한 네트워크 관리 객체는 한 번만 생성되면되므로 Hilt를 이용하여 앱 전체에서 싱글톤으로 의존성으로 제공합니다.
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로 등록을 해제하고 리소스를 정리합니다.
trySend
함수를 통해 채널에 false를 추가하여 callbackFlow가 false를 방출하도록 합니다. trySend
함수를 통해 채널에 true를 추가하여 callbackFlow가 true를 방출하도록 합니다. registerDefaultNetworkCallback을 통해 NetworkCallback 등록은 앱 당 최대 100개로 제한 됩니다. 만약 제한을 초과하면 예외(Exception)가 발생합니다.
이 문제를 방지하기 위해 더 이상 필요하지 않은 네트워크 콜백은 반드시 unregisterNetworkCallback(ConnectivityManager.NetworkCallback)을 사용해 등록 해제를 해야 합니다.
registerDefaultNetworkCallback는 Api 24에 추가 되었습니다. 만약 min sdk를 24보다 밑이라면 registerNetworkCallback로 네트워크 콜백을 등록해야 합니다. (registerNetworkCallback는 Api 21)
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를 보여주고 싶다면 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