본문 바로가기
Development/Android

[Android] Jetpack Compose + Flow로 네트워크 연결 상태 처리를 해보자.

by du.it.ddu 2021. 12. 26.
반응형

저는 안드로이드 관련된 Medium 포스팅을 메일로 전달받고 있습니다.
최근 제목과 같이 Jetpack ComposeFlow를 활용하여 네트워크 연결 상태 처리를 하는 포스팅을 보고 예제를 간단하게 작성하여 포스팅하려 합니다.

참고한 Medium 링크는 아래와 같으니, 원문을 보고 싶다면 참고하면 됩니다.
https://attilaakinci.medium.com/network-connectivity-on-compose-a35f6efa1a5c

 

Network Connectivity on Jetpack Compose

Brief story on network connectivity usages on modern android development style with jetpack compose support

attilaakinci.medium.com


또한 포스팅에 사용된 예제는 아래 Github에 저장되어 있으니, 전체 코드가 필요한 경우 참고하면 됩니다.
https://github.com/DuItDDu/Android-Compose-Network-Handling

 

GitHub - DuItDDu/Android-Compose-Network-Handling

Contribute to DuItDDu/Android-Compose-Network-Handling development by creating an account on GitHub.

github.com


개요

 

패키지 구조는 위와 같습니다. 각각의 클래스는 아래 역할을 합니다.

- MyApplication : Hilt 적용을 위한 Application 클래스
- MainActivity, MainViewModel : 앱의 화면 처리를 위한 클래스
- NetworkOfflineDialog : 네트워크 연결이 되어있지 않은 경우 표시할 다이얼로그
- AppModule : Hilt를 적용하여 NeworkChecker 의존성 주입을 위한 클래스
- NetworkChecker, NetworkState : 네트워크 상태 처리 클래스


네트워크 상태 처리

이 포스팅의 핵심이 되는 내용입니다.

- NetworkState

sealed class NetworkState {
    object None: NetworkState()
    object Connected: NetworkState()
    object NotConnected: NetworkState()
}

Kotlin의 sealed class를 사용하여 네트워크 상태를 정의합니다.
최초 상태인 None, 연결 상태의 Connected, 비연결 상태의 NotConnected 세 상태로 이루어져 있습니다.

- NetworkChecker

class NetworkChecker @Inject constructor(
    private val appContext: Context
) {
    private val _networkState = MutableStateFlow<NetworkState>(NetworkState.None)
    val networkState: StateFlow<NetworkState> = _networkState

    private val validTransportTypes = listOf(
        NetworkCapabilities.TRANSPORT_WIFI,
        NetworkCapabilities.TRANSPORT_CELLULAR
    )

    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            super.onAvailable(network)
            _networkState.value = NetworkState.Connected
        }

        override fun onLost(network: Network) {
            super.onLost(network)
            _networkState.value = NetworkState.NotConnected
        }
    }

    private val connectivityManager: ConnectivityManager? =
        appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager

    init {
        connectivityManager?.run {
            initiateNetworkState(this)
            registerNetworkCallback(this)
        }
    }

    private fun initiateNetworkState(manager: ConnectivityManager) {
        _networkState.value = manager.activeNetwork?.let {
            manager.getNetworkCapabilities(it)
        }?.let { networkCapabilities ->
            if (validTransportTypes.any { networkCapabilities.hasTransport(it) }) {
                NetworkState.Connected
            } else {
                NetworkState.NotConnected
            }
        } ?: NetworkState.NotConnected
    }

    private fun registerNetworkCallback(manager: ConnectivityManager) {
        NetworkRequest.Builder().apply {
            validTransportTypes.onEach { addTransportType(it) }
        }.let {
            manager.registerNetworkCallback(it.build(), networkCallback)
        }
    }
}

코드가 짧지는 않지만, 간단합니다.

우선, "android.permission.ACCESS_NETWORK_STATE" 퍼미션을 메니페스트에 추가해줍니다.

생성자로 Context를 주입받고, 안드로이드 프레임워크의 ConnectivityManager 객체를 획득합니다.
그리고 네트워크 상태 콜백을 처리하고, 앞서 정의한 네트워크 상태를 판단하고 Flow를 사용하여 처리합니다.

Flow를 사용하여 외부에서 이를 구독하고, 상태 처리를 할 수 있습니다.
LiveData를 사용하면 되지 않느냐에 대한 질문이 들어올 수 있는데, ViewModel 외부에서 LiveData를 사용하는 것은 권장되지 않습니다.


의존성 주입

네트워크 상태 처리 객체를 사용하기 전에, 의존성 주입 세팅을 해줍니다.
이 프로젝트에선 의존성 주입을 위한 프레임워크로 Hilt를 사용합니다.

아래 문서를 참고하여 Hilt를 위한 gradle 세팅을 해 줍니다.
https://developer.android.com/training/dependency-injection/hilt-android?hl=ko

 

Hilt를 사용한 종속 항목 삽입  |  Android 개발자  |  Android Developers

Hilt를 사용한 종속 항목 삽입 Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. 수동 종속 항목 삽입을 실행하려면 모든 클래스

developer.android.com


- Application 클래스 작성

@HiltAndroidApp
class MyApplication : Application()

// AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.duitddu.android.compose.network.handling">

    ...
    
    <application
        ...
        android:name=".app.MyApplication">
        
        ...
    </application>

</manifest>

Application을 상속받은 클래스를 하나 생성하고, @HiltAndroidApp 어노테이션을 추가해줍니다.
그리고 AndroidManifestApplication 클래스를 설정합니다.

- AppModule 클래스 작성

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

    @Provides
    @Singleton
    fun provideNetworkChecker(
        @ApplicationContext context: Context
    ) : NetworkChecker = NetworkChecker(context)
}

해당 클래스는 object로 선언됩니다.
클래스에 @Module 어노테이션을 사용하여 Hilt에서 사용하는 모듈임을 표시합니다.
@InstallIn(SingletonCompoenet::class) 어노테이션을 사용하여 앱 전역에서 싱글턴 객체로 사용됨을 표시합니다.

그리고 내부에 NetworkChecker를 생성하는 함수를 만듭니다. 이때, 생성자로 필요한 Context@ApplicationContext 어노테이션을 사용합니다.

@Provides 어노테이션을 사용해 객체 제공함수임을 표시하고, @Singletone 어노테이션을 사용하여 싱글턴 객체로 제공될 수 있도록 합니다.


다이얼로그 작성

네트워크 미연결을 표시할 다이얼로그를 Jetpack Compose를 이용해서 작성해보겠습니다.

@Composable
fun NetworkOfflineDialog(
    networkState: NetworkState,
    onRetry: () -> Unit
) {
    if (networkState is NetworkState.NotConnected) {
        AlertDialog(
            onDismissRequest = {},
            title = { Text(text = "인터넷이 연결되지 않았습니다.") },
            text = { Text(text = "인터넷 연결을 확인하신 후 재시도 버튼을 눌러주세요.") },
            confirmButton = {
                TextButton(onClick = onRetry) {
                    Text(text = "재시도")
                }
            }
        )
    }
}

@Composable 어노테이션을 함수에 사용하여 Composable임을 표시합니다.
파라미터로 NetworkState와 "재시도" 버튼의 액션을 Lambda로 받습니다.

내부에서 상태가 NotConnected 일때만 처리하도록 되어 있으며 텍스트 등은 편의상 하드코딩 되어 있습니다.
상세한 코드는 변경되어도 무관합니다.


UI처리

- MainViewModel

@HiltViewModel
class MainViewModel @Inject constructor(
    private val networkChecker: NetworkChecker
) : ViewModel() {
    private val _networkState = MutableSharedFlow<NetworkState>(replay = 1)
    val networkState: SharedFlow<NetworkState> = _networkState

    init {
        viewModelScope.launch {
            networkChecker.networkState.collectLatest {
                _networkState.emit(it)
            }
        }
    }

    fun onRetry() {
        // Do something.
    }
}

의존성 주입으로 Hilt를 사용하기 때문에 @HiltViewModel 어노테이션을 적용하며, 생성자에 @Inject 어노테이션을 적용합니다.

MainViewModel은 생성자 파라미터로 NetworkChecker를 받으며, 이는 앞서 정의한 Hilt 모듈에 의해 주입됩니다.
MainViewModel 초기화 시, NetworkCheckernetworkState FlowviewModelScope에서 구독합니다.
그리고 UI의 상태를 관리하는 SharedFlow에 값을 전파합니다.

- MainActivity

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeNetworkHandlingTheme {
                Surface(color = MaterialTheme.colors.background) {
                    MainPage(viewModel)
                }
            }
        }
    }
}

@Composable
fun MainPage(viewModel: MainViewModel) {
    val networkState by viewModel.networkState.collectAsState(initial = NetworkState.None)

    NetworkOfflineDialog(networkState = networkState) {
        viewModel.onRetry()
    }
}

Hilt를 사용하는 객체들을 사용하기 때문에, @AndoridEntryPoint 어노테이션을 적용 해 주어야 합니다.

MainActivity는 화면을 꾸미고, MainViewModel의 네트워크 상태를 구독하고, 상태 변경 시 NetworkOfflineDialog를 화면에 그려내게 됩니다.

MainViewModelnetworkState(SharedFlow)State로 구독하게 되며, 상태가 변경되면 자동으로 반영됩니다.


마치며

Jetpack ComposeFlow를 사용하여 네트워크 상태 처리를 하는 방법을 알아보았습니다.

UI를 간편하고 빠르게 작성할 수 있는 도구인 Jetpack Compose와 안드로이드 프레임워크에 얽매이지 않고 데이터의 변경과 변경 처리가 용이한 Flow를 사용하면 네트워크 상태 처리 뿐만 아니라 앱의 다양한 상태에 대한 처리가 간편해집니다.

앱의 상태 처리에 대한 고민이 있다면 Flow를 사용해보는 것이 좋은 방법이 될 수 있습니다.

이 포스팅이 도움이 되었으면 좋겠습니다. :)

반응형