관리 메뉴

나만을 위한 블로그

[Android] Flow, LiveData를 써서 네트워크 연결 상태를 확인하는 방법 본문

Android

[Android] Flow, LiveData를 써서 네트워크 연결 상태를 확인하는 방법

참깨빵위에참깨빵 2023. 1. 20. 01:20
728x90
반응형

※ 이 방법이 정답이 아니고 다른 방법들이 많으니 찾아보자

 

2년 전 자바, 코틀린 별 네트워크 연결 상태를 처리하는 방법에 대해 포스팅한 적이 있다.

 

https://onlyfor-me-blog.tistory.com/322

 

[Android] 네트워크 연결 상태를 확인하는 방법(JAVA + Kotlin)

네트워크 예외처리를 하다 보면 현재 기기에 인터넷이 연결된 상태인지를 확인해야 할 수 있다. 연결된 상태면 다음 로직을 이어서 수행하거나, 연결되지 않은 상태라면 다이얼로그를 띄워서

onlyfor-me-blog.tistory.com

 

그러나 여기서 사용하는 코드가 구식인 것도 있고, Flow, LiveData를 사용해 네트워크 연결 상태를 확인하는 포스팅을 미디엄에서 봐서 새로 포스팅하려고 한다.

내가 본 미디엄 포스팅 링크는 아래에 있다.

 

https://markonovakovic.medium.com/android-better-internet-connection-monitoring-with-kotlin-flow-feac139e2a3

 

Android, better Internet connection monitoring with Kotlin Flow

This topic has been done into the ground over the years. Why then am I writing this post? I want to present you with Kotlin Flow example…

markonovakovic.medium.com

 

먼저 로그 찍기 위해 Timber 라이브러리를 사용했기 때문에 앱 gradle에 아래 문구를 넣어준다. 만약 쓰기 싫다면 건너뛰어도 된다.

implementation 'com.jakewharton.timber:timber:5.0.1'

 

그리고 Application을 상속한 클래스에 함수를 만들고, 해당 클래스의 onCreate() 안에 이 함수를 넣어서 Timber 사용 준비를 끝낸다.

 

import android.app.Application
import com.example.kotlinprac.BuildConfig
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber

@HiltAndroidApp
class BaseApplication: Application() {
    override fun onCreate() {
        super.onCreate()

        if (BuildConfig.DEBUG) {
            Timber.plant(AppDebugTree())
        }
    }

    private class AppDebugTree: Timber.DebugTree() {
        override fun createStackElementTag(element: StackTraceElement): String =
            "${element.fileName}:${element.lineNumber}:${element.methodName}"
    }
}

 

BuildConfig 밑에 빨간 줄이 뜰 텐데 내 패키지명과 본인의 패키지명이 일치하지 않아서 생기는 현상이다.

com.example.kotlinprac 임포트문을 지우고 본인의 BuildConfig 경로를 넣으면 에러는 발생하지 않는다. 그 다음 매니페스트에 등록한다.

 

<application
        android:name=".BaseApplication"
        android:allowBackup="true"
        .
        .
        .

 

이제 코틀린 파일에 유틸 함수를 만든다.

 

import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

@FlowPreview
inline fun <Result> Flow<NetworkStatus>.map(
    crossinline onUnavailable: suspend () -> Result,
    crossinline onAvailable: suspend () -> Result,
): Flow<Result> = map { status ->
    when (status) {
        NetworkStatus.Unavailable -> onUnavailable()
        NetworkStatus.Available -> onAvailable()
    }
}

 

원래 flatMapConcat {}을 사용한 flatMap()도 있었지만 해당 예제에서 사용하지 않기 때문에 뺐다.

그리고 네트워크 상태 구별 시 사용할 sealed class를 만든다. 이것은 한 파일에 만들었다.

 

sealed class NetworkStatus {
    object Available: NetworkStatus()
    object Unavailable: NetworkStatus()
}

sealed class MyState {
    object Fetched: MyState()
    object Error: MyState()
}

 

다음으로 네트워크 상태를 추적하는 Tracker 클래스를 만든다.

 

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import timber.log.Timber

class NetworkStatusTracker(context: Context) {
    private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val networkStatus = callbackFlow {
        val networkStatusCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onUnavailable() {
                super.onUnavailable()
                Timber.e("onUnavailable()")
                trySend(NetworkStatus.Unavailable).isSuccess
            }

            override fun onAvailable(network: Network) {
                super.onAvailable(network)
                Timber.e("onAvailable()")
                trySend(NetworkStatus.Available).isSuccess
            }

            override fun onLost(network: Network) {
                Timber.e("onLost()")
                trySend(NetworkStatus.Unavailable).isSuccess
            }
        }

        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()
        connectivityManager.registerNetworkCallback(request, networkStatusCallback)

        awaitClose {
            connectivityManager.unregisterNetworkCallback(networkStatusCallback)
        }
    }.distinctUntilChanged()
}

 

이 클래스가 있어야 네트워크 연결 상태를 추적해서 각 상태마다 로그캣에 로그를 띄울 수 있다.

그리고 뷰모델을 만들어 네트워크 상태를 받아 LiveData로 변환하게 한다.

 

import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.Dispatchers

class NetworkStatusViewModel(
    networkStatusTracker: NetworkStatusTracker
): ViewModel() {
    val state = networkStatusTracker.networkStatus.map(
        onAvailable = { MyState.Fetched },
        onUnavailable = { MyState.Error }
    ).asLiveData(Dispatchers.IO)
}

 

이제 준비가 끝났으니 액티비티를 수정한다. XML은 저번에 사용한 XML을 그대로 가져왔다.

 

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:animateLayoutChanges="true"
        tools:context=".networkstate.NetworkStateTestActivity">

        <LinearLayout
            android:id="@+id/layoutDisconnected"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:gravity="center">

            <ImageView
                android:layout_width="200dp"
                android:layout_height="200dp"
                android:src="@drawable/ic_baseline_wifi_off_24"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text="DISCONNECTED"
                android:textAllCaps="true"
                android:textColor="@android:color/holo_red_dark"
                android:textSize="24sp"
                android:textStyle="bold"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="40dp"
                android:layout_marginTop="8dp"
                android:layout_marginEnd="40dp"
                android:gravity="center"
                android:text="인터넷에 연결되어 있지 않습니다"
                android:textSize="18sp"/>

        </LinearLayout>

        <LinearLayout
            android:id="@+id/layoutConnected"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:visibility="gone"
            android:gravity="center">

            <ImageView
                android:layout_width="200dp"
                android:layout_height="200dp"
                android:src="@drawable/ic_baseline_wifi_24"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text="DISCONNECTED"
                android:textAllCaps="true"
                android:textColor="@android:color/holo_green_dark"
                android:textSize="24sp"
                android:textStyle="bold"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="40dp"
                android:layout_marginTop="8dp"
                android:layout_marginEnd="40dp"
                android:gravity="center"
                android:text="인터넷에 연결되어 있습니다"
                android:textSize="18sp"/>

        </LinearLayout>

    </FrameLayout >

</layout>
import android.os.Bundle
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.kotlinprac.databinding.ActivityNetworkStateTestBinding

class NetworkStateTestActivity : AppCompatActivity() {

    private val binding: ActivityNetworkStateTestBinding by lazy {
        ActivityNetworkStateTestBinding.inflate(layoutInflater)
    }
    private val viewModel: NetworkStatusViewModel by lazy {
        ViewModelProvider(this, object : ViewModelProvider.Factory {
            override fun <T : ViewModel> create(modelClass: Class<T>): T {
                val networkStatusTracker = NetworkStatusTracker(this@NetworkStateTestActivity)
                return NetworkStatusViewModel(networkStatusTracker) as T
            }
        })[NetworkStatusViewModel::class.java]
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        viewModel.state.observe(this) { state ->
            when (state) {
                MyState.Fetched -> {
                    binding.layoutConnected.visibility = VISIBLE
                    binding.layoutDisconnected.visibility = GONE
                }
                MyState.Error -> {
                    binding.layoutConnected.visibility = GONE
                    binding.layoutDisconnected.visibility = VISIBLE
                }
            }
        }
    }
}

 

뷰모델을 초기화하고, 뷰모델의 LiveData를 액티비티에서 관찰해서 받는 값(Fetched, Error)에 따라 레이아웃을 보여주거나 숨김 처리한다.

앱을 실행하면 지난번과 똑같이 작동한다. 네트워크 연결 상태에 따라 로그캣에 다른 메시지가 표시되는 것도 볼 수 있다.

 

 

반응형
Comments