[Android] - 네트워크 연결 관리하기(ConnectivityManager)
프로젝트에서 Network 연결 상태에 따라 API 동작 및 화면을 갱신해야 했습니다.
여러 가지 방법이 있지만, 저는 공식문서에 나와있는 ConnectivityManager를 통해서 해결했습니다.
설명하기 전 네트워크 사용을 위해선 안드로이드 매니페스트에 다음 권한을 포함해야 합니다.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
해당 권한은 음악, 갤러리 접근과 달리 일반권한이기 때문에 런타임에 요청할 필요가 없습니다.
즉 어플이 설치됨에 따라 자동으로 권한이 추가됩니다.
추가되는 권한은 다음과 같은 일을 합니다.
- android.permission.INTERNET : 애플리케이션에서 네트워크 소켓을 열기 위해 필요함
- android.permission.ACCESS_NETWORK_STATE : 애플리케이션에서 네트워크에 관한 정보에 접근하기 위해 필요함
저는 앱의 네트워크 연결의 변경사항을 추적하기 위해 ConnectivityManager를 사용했습니다.
ConnectivityManager는 앱에 시스템의 연결 상태를 알려줍니다.
우선 ConnectivityManager의 인스턴스를 가져오기 위해선 context가 필요합니다.
저는 Hilt 모듈을 이용했기 때문에 다음과 같은 코드를 작성했습니다.
@Module
@InstallIn(SingletonComponent::class)
object ConnectivityManagerModule {
@Singleton
@Provides
fun provideConnectivityManager(@ApplicationContext context: Context): ConnectivityManager {
return context.getSystemService(ConnectivityManager::class.java)
}
}
애플리케이션이 실행되는 동안 네트워크 연결 상태를 감지하기 위해서 ApplicationContext를 사용했습니다.
네트워크 이벤트를 추적하기 위해선 NetworkCallback 클래스가 필요합니다.
connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network : Network) {
Log.e(TAG, "The default network is now: " + network)
}
override fun onLost(network : Network) {
Log.e(TAG, "The application no longer has a default network. The last default network was " + network)
}
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
Log.e(TAG, "The default network changed capabilities: " + networkCapabilities)
}
override fun onLinkPropertiesChanged(network : Network, linkProperties : LinkProperties) {
Log.e(TAG, "The default network changed link properties: " + linkProperties)
}
})
각각이 의미하는 바는 다음과 같습니다.
- onAvailable : 네트워크가 연결되어 이용이 가능할 때
- onLost : 네트워크 연결이 끊겨, 이용이 불가능할 때
- onCapabilitiesChanged : 네트워크 기능이 변경되었을 때
- onLinkPropertiesChanged : 기본 네트워크의 링크 속성이 변경되었을 때
해당 내용을 바탕으로 Network의 연결을 감지하는 NetworkManager 클래스를 구현했습니다.
class NetworkManager @Inject constructor(private val connectivityManager: ConnectivityManager) {
private val _isConnected = MutableStateFlow(false)
val isConnected: StateFlow<Boolean> = _isConnected
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
_isConnected.value = true
}
override fun onLost(network: Network) {
_isConnected.value = false
}
}
fun registerNetworkCallback() {
connectivityManager.registerDefaultNetworkCallback(networkCallback)
}
fun unRegisterNetworkCallback() {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}
저는 네트워크의 연결을 지속적으로 관찰해야 했기에, stateFlow로 선언해서, 네트워크 연결의 변화가 생길 경우 value를 업데이트해 줬습니다.
unRegisterNetworkCallback 함수는 더 이상 콜백이 필요 없을 경우 호출하며, callback을 해제합니다.
NetworkManager를 어떤 식으로 사용해야 할지 고민을 해봤습니다.
모든 Activity나 Fragment가 네트워크 연결의 상태를 알아야 했고, 적절한 위치에 NetworkManager를 위치시켜서
isConnecetd의 값에 따라 UI를 변경시켜야 했습니다.
그래서 저는 viewModel의 NetworkManager를 주입해서, viewModel에서 네트워크 연결을 관찰했습니다.
@HiltViewModel
class testViewModel @Inject constructor(private val networkManager: NetworkManager) : ViewModel(){
init{
networkManager.registerNetworkCallback()
}
private val _isConnected = MutableStateFlow(false)
val isConnected: StateFlow<Boolean> = _isConnected
private fun observerNetworkConnection() {
viewModelScope.launch {
networkManager.isConnected.collectLatest { isConnected ->
_isConnected.update { isConnected }
}
}
}
override fun onCleared(){
super.onCleared()
networkManager.unRegisterCallback()
}
}
ViewModel의 생명주기에 따라 callback을 등록하고, 해제하는 작업을 포함시켰습니다.
이제 이렇게 viewModel의 isConnected값을 통해서 Fragment나 Activity에서 UI를 변경하거나, API 접근을 제한해야 합니다.
우선 네트워크 연결이 끊겼을 때의 화면 변경을 위해 UI를 구현해야 했고, 저는 Compose를 이용한 Dialog를 선택했습니다.
Compose에서는 CircularProgressIndicator를 통해서 간단하게 로딩 화면을 구현할 수 있었습니다.
@Composable
fun LoadingDialogScreen() {
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val dialogWidth = screenWidth * 0.8f
Dialog(onDismissRequest = {}) {
Column(
modifier = Modifier
.width(dialogWidth)
.height(screenHeight),
verticalArrangement = Arrangement.Center,
) {
Row(
modifier = Modifier
.align(Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
CircularProgressIndicator(
modifier = Modifier.size(100.dp),
color = Color.LightGray,
strokeWidth = 10.dp,
)
}
Spacer(modifier = Modifier.height(30.dp))
Row(
modifier = Modifier.align(Alignment.CenterHorizontally),
) {
Text(
text = stringResource(id = R.string.disconnected_network_message),
style = MaterialTheme.typography.displaySmall,
)
}
}
}
}
XML도 유사하게 XMl을 생성하고, DialogFragment를 생성해서 navigate를 구현하면 쉽게 할 수 있습니다.
이렇게 구현한 Dialog를 Composable 함수에서 호출하기 위해선 네트워크 연결 상태를 알아야 합니다.
val isConnected by boardListViewModel.isConnected.collectAsStateWithLifecycle()
isConnected가 변경될 때마다, Composable 함수를 다시 실행하기 위해 collectAsStateWithLifeCycle()을 사용했습니다.
if (isConnected.not()) {
LoadingDialogScreen()
}
네트워크 연결이 끊겼을 경우 LoadingDialog를 호출하는 것으로 해결했습니다.
네트워크 연결을 감지하기 위해서 broadcast receiver를 사용해야 하는 줄 알았는데,
더 편리한 ConnectivityManager가 있는 것을 알게 된 것 같습니다.
애플리케이션 특성상 네트워크의 유형별 다양한 처리보단 상태 정보만 필요했기에, 간단하게 구현했는데 공식문서를 다시 차근차근 읽어봐야 할 것 같습니다.. ㅎ
참고