본문 바로가기
안드로이드 코틀린

[Kotlin][Android] 네트워크 모니터링 기능

by teamnova 2021. 11. 27.

안녕하세요. 이번 시간에는 사용자가 네트워크 환경에서 벗어난 경우에 알림 메시지를 띄워주는 기능을 구현하도록 하겠습니다.

 

우선 인터넷을 감지하는 녀석이 누군지 알아봐야겠죠?

안드로이드에서는ConnectivityManager Class를 통해 사용자의 모바일 기기가 인터넷에 연결되어 있는지 확인하고 연결되어 있다면 어떤 유형의 연결이 설정되어 있는지 확인할 수 있으며 추가적으로 인터넷이 끊긴 상태또한 알 수 있습니다.

 

https://developer.android.com/training/monitoring-device-state/connectivity-status-type?hl=ko 

 

연결 상태 및 연결 측정 모니터링  |  Android 개발자  |  Android Developers

연결 상태 및 연결 측정 모니터링 ConnectivityManager를 사용하여 인터넷에 연결되어 있는지 확인하고 연결되어 있다면 어떤 유형의 연결이 설정되어 있는지 확인할 수 있습니다. 인터넷에 연결되어

developer.android.com

인터넷 연결 여부를 알 수 있는 코드는 아래 나와 있는 메서드를 사용해 활성 네트워크를 대화식으로 쿼리하여 인터넷에 연결되어 있는지 확인할 수도 있습니다.

    val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
    val isConnected: Boolean = activeNetwork?.isConnectedOrConnecting == true

 

네트워크 연결에 핵심 요소를 좀 더 알아보면 ConnectivityManager 외에도 ConnectivityManager.NetworkCallback이 있습니다. 각 클래스의 역할은 다음과 같습니다.

 

public class ConnectivityManager 

  • 네트워크 연결 상태에 대한 쿼리에 응답하는 클래스입니다
  • 네트워크 연결이 변경될 때 응용프로그램에게 알려줍니다.

https://developer.android.com/reference/android/net/ConnectivityManager

 

ConnectivityManager  |  Android Developers

 

developer.android.com

 

public static class ConnectivityManager.NetworkCallback

NetworkRequest 에 대해 콜백하는 클래스입니다.

 

https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback?hl=en 

 

ConnectivityManager.NetworkCallback  |  Android Developers

 

developer.android.com

 

저는 이 네트워크의 값이 바뀌는 것을 LiveData를 사용하여 데이터가 바뀌었을 때 듣도록 구현을 하였습니다.

 

작동원리는 다음과 같습니다.

 

우선 NetworkCallback 정의를 해줍니다.

/** 목표 : [networkCallback] 콜백 함수 정의
 *
 * 네트워크 감지 여부에 따라 로직이 진행한다.
* */
private fun createNetworkCallback() = object : ConnectivityManager.NetworkCallback() {

    /** 네트워크가 감지되면 호출
     * 프레임워크가 연결되고 사용할 준비가 된 새 네트워크를 선언하면 호출된다.
     * */
    override fun onAvailable(network: Network) {
        Logger.d("onAvailable: ${network}")
        val networkCapabilities = cm.getNetworkCapabilities(network)
        val hasInternetCapability = networkCapabilities?.hasCapability(NET_CAPABILITY_INTERNET)
        if (hasInternetCapability == true) {
            // 인터넷이 실제로 되는지 체크
            CoroutineScope(Dispatchers.IO).launch {
                // 인터넷 연결이 잘 되는지 체크
                val hasInternet = DoesNetworkHaveInternet.execute(network.socketFactory)
                // 인터넷이 연결된 경우
                if(hasInternet){
                    withContext(Dispatchers.Main){
                        validNetworks.add(network)
                        checkValidNetworks()
                    }
                }
            }
        }
    }

    /** 네트워크 감지가 안되면 호출
     *
     * 사용자가 네트워크가 연결을 끊거나 이 요청 또는 콜백이 더 이상 충족하지 않을 때 호출된다.
     */
    override fun onLost(network: Network) {
        Logger.d("onLost 실행 : 네트워크($network)")
        validNetworks.remove(network)
        checkValidNetworks()
    }
}

 

2. LiveData가 Active일 때 ConnectivityManager 가 콜백 객체(NetworkCallback) 등록

/** 목표 : [networkCallback] 콜백 객체 등록
 *
 * 지정한 [networkRequest]를 충족하는 모든 네트워크에 대한 알림을 듣도록 콜백 객체[networkCallback]를 등록한다.
 * 해당 콜백은 애플리케이션이 종료되거나 unregisterNetworkCallback() 함수가 호출 될때 까지 계속 호출한다.
 */
override fun onActive() {
    networkCallback = createNetworkCallback()
    // networkRequest 설정
    val networkRequest = NetworkRequest.Builder()
        .addCapability(NET_CAPABILITY_INTERNET)
        .build()
    // networkCallback 콜백 객체 등록
    cm.registerNetworkCallback(networkRequest, networkCallback)
}

3. LiveData가 InActive일 때 ConnectivityManager 가 콜백 객체(NetworkCallback) 해제

/** 목표 : [networkCallback] 콜백 객체 해제
 *
 * requestNetwork(), registerNetworkCallback() 함수로 등록된 [networkCallback] 콜백 객체 해제함(disconnect).
 */
override fun onInactive() {
    // networkCallback 콜백 해제
    cm.unregisterNetworkCallback(networkCallback)
}

실제로 인터넷이 되는지 확인하는 객체를 아래처럼 만들어 줍니다.

object DoesNetworkHaveInternet {

  private val TAG = "DoesNetworkHaveInternet"

  fun execute(socketFactory: SocketFactory): Boolean {
    return try{
      Log.d(TAG, "execute() | PINGING google ")
      val socket = socketFactory.createSocket() ?: throw IOException("Socket is null.")
      socket.connect(InetSocketAddress("8.8.8.8", 53), 1500)
      socket.close()
      Log.d(TAG, "execute() | 인터넷 연결 O ")
      true
    }catch (e: IOException){
      Log.d(TAG, "인터넷 연결 X : 에러${e}")
      false
    }
  }
}

 

이렇게 정의한 것을 LiveData의 onActive(), onInactive()메서드에 등록을 해줍니다.

/**
     * 목표 : [networkCallback] 콜백 객체 등록
     *
     * 지정한 [networkRequest]를 충족하는 모든 네트워크에 대한 알림을 듣도록 콜백 객체[networkCallback]를 등록한다.
     * 해당 콜백은 애플리케이션이 종료되거나 unregisterNetworkCallback() 함수가 호출 될때 까지 계속 호출한다.
     * */
    override fun onActive() {
        networkCallback = createNetworkCallback()
        // networkRequest 설정
        val networkRequest = NetworkRequest.Builder()
            .addCapability(NET_CAPABILITY_INTERNET)
            .build()
        // networkCallback 콜백 등록
        cm.registerNetworkCallback(networkRequest, networkCallback)
    }

    /** 목표 : [networkCallback] 콜백 객체 해제
     *
     * requestNetwork(), registerNetworkCallback() 함수로 등록된 [networkCallback] 콜백 객체 해제함(disconnect).
     */
    override fun onInactive() {
        // networkCallback 콜백 해제
        cm.unregisterNetworkCallback(networkCallback)
    }

그리고 데이터가 달라지게 되면 아래와 같이 데이터가 바뀌었다고 LiveData의 postValue()를 호출해줍시다.

 private fun checkValidNetworks() {
        Log.d(TAG, "checkValidNetworks() | 네트워크 쌓인 개수 : (${validNetworks.size}) ")
        postValue(validNetworks.size > 0)
    }

 

이제 LiveData를 듣고자 하는 Activity로 와서 아래와 같이 LiveData의 Observer를 등록해주도록 합시다.

등록은 Activity의 onStart에 해주시면 됩니다.

 connectionLiveData.observe(lifecycleOwner, { isConnected ->
      if (isConnected){
        Logger.d("네트워크 연결 성공")
      }else{
        Logger.d("네트워크 연결 실패")
        showSnackBar(view)
      }
    })

해제시에는 아래와 같이 작성해줍시다. Acivity의 onDestroyed에서 실행해주면 됩니다.

    connectionLiveData.removeObservers(lifecycleOwner)

 

스낵바는 아래처럼 쓰시면 됩니다.

  /** 인터넷 끊김 알림 스낵바 생성
   * */
  private fun showSnackBar(view: View){
    val snackBarView = Snackbar.make(view, R.string.common_network_disconnected , Snackbar.LENGTH_LONG)
    val view = snackBarView.view
    val params = view.layoutParams as FrameLayout.LayoutParams
    params.gravity = Gravity.TOP
    view.layoutParams = params
    //view.background = ContextCompat.getDrawable(this , R.drawable.custom_drawable) // for custom background
    snackBarView.animationMode = BaseTransientBottomBar.ANIMATION_MODE_FADE
    snackBarView.show()
  }

 

코드안에 있는 Logger은 밑의 스틱코드를 이용하면 쉽게 불러와서 사용할 수 있습니다.

https://stickode.com/detail.html?no=2559 

 

스틱코드

 

stickode.com

 

결과화면입니다.