Skip to content

Instantly share code, notes, and snippets.

@marenovakovic
Last active October 23, 2023 16:57
Show Gist options
  • Star 25 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save marenovakovic/9466bfc1b765e06e1b3600efbc4da2f8 to your computer and use it in GitHub Desktop.
Save marenovakovic/9466bfc1b765e06e1b3600efbc4da2f8 to your computer and use it in GitHub Desktop.
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.map
class MainActivity : AppCompatActivity() {
private val viewModel: NetworkStatusViewModel by lazy {
ViewModelProvider(
this,
object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val networkStatusTracker = NetworkStatusTracker(this@MainActivity)
return NetworkStatusViewModel(networkStatusTracker) as T
}
},
).get(NetworkStatusViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.state.observe(this) { state ->
findViewById<TextView>(R.id.textView).text = when (state) {
MyState.Fetched -> "Fetched"
MyState.Error -> "Error"
}
}
}
}
sealed class MyState {
object Fetched : MyState()
object Error : MyState()
}
class NetworkStatusViewModel(
networkStatusTracker: NetworkStatusTracker,
) : ViewModel() {
val state =
networkStatusTracker.networkStatus
.map(
onAvailable = { MyState.Fetched },
onUnavailable = { MyState.Error },
)
.asLiveData(Dispatchers.IO)
}
sealed class NetworkStatus {
object Available : NetworkStatus()
object Unavailable : NetworkStatus()
}
class NetworkStatusTracker(context: Context) {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkStatus = callbackFlow<NetworkStatus> {
val networkStatusCallback = object : ConnectivityManager.NetworkCallback() {
override fun onUnavailable() {
println("onUnavailable")
offer(NetworkStatus.Unavailable)
}
override fun onAvailable(network: Network) {
println("onAvailable")
offer(NetworkStatus.Available)
}
override fun onLost(network: Network) {
println("onLost")
offer(NetworkStatus.Unavailable)
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose {
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
}
}
.distinctUntilChanged()
}
@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()
}
}
@FlowPreview
inline fun <Result> Flow<NetworkStatus>.flatMap(
crossinline onUnavailable: suspend () -> Flow<Result>,
crossinline onAvailable: suspend () -> Flow<Result>,
): Flow<Result> = flatMapConcat { status ->
when (status) {
NetworkStatus.Unavailable -> onUnavailable()
NetworkStatus.Available -> onAvailable()
}
}
@raamcosta
Copy link

Great gist and great article! I'm just curious, why not use normal enum classes if all sealed class subclasses are just objects?

@marenovakovic
Copy link
Author

Thank you very much, am glade you like it.
I prefer sealed class over enum class. That's it. I use enum class regularly just chose sealed class here. In this case either one is valid.

@dugsmith
Copy link

dugsmith commented Apr 23, 2021

This is super helpful — thank you for generously taking the time to share such an important example!

@dugsmith
Copy link

And one tiny request: if you could add the imports at the top of the file, it would help clarify where everything comes from.

@marenovakovic
Copy link
Author

Am glad you find this helpful! Thank you for kind words. I will add imports, thanks for suggestion.

@PatricioIN
Copy link

PatricioIN commented Oct 7, 2021

The first time you enter it is not called to know the status of the connection, it is only called when it changes. Only happens when you dont have connection. Google issue? https://issuetracker.google.com/issues/144891976

@thotluna
Copy link

thotluna commented Apr 6, 2022

excellent code

@marenovakovic
Copy link
Author

@thotluna thank you very much. am really glad you like it!

@marenovakovic
Copy link
Author

@PatricioIN I didn't encounter any issues. I will retest it and update gist if I encounter any errors

@gsrathoreniks
Copy link

gsrathoreniks commented Jun 24, 2022

If your requirement is "NOTIFY only if no network connection exists neither Wifi nor Mobile Internet or one of them exists" then this modification can be helpful --> NetworkCheck

@Drjacky
Copy link

Drjacky commented Aug 26, 2022

The first time you enter it is not called to know the status of the connection, it is only called when it changes. Only happens when you dont have connection. Google issue?

The same issue as @PatricioIN
It doesn't trigger for the first time. Although, I use this collectIn directly to collect the emission:

inline fun <T> Flow<T>.collectIn(
    owner: LifecycleOwner,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    coroutineContext: CoroutineContext = EmptyCoroutineContext,
    crossinline action: suspend (T) -> Unit,
) = owner.lifecycleScope.launch(coroutineContext) {
    owner.lifecycle.repeatOnLifecycle(minActiveState) {
        collect {
            action(it)
        }
    }
}

but when I change the wifi/data, it works!


Update:
I followed what you did and still, it doesn't work when the internet is already disconnected.


Update:
I had to add connectivityManager.requestNetwork(request, networkStatusCallback, 1000) too, now it works 🎉

@sralpert
Copy link

I'm just starting to tinker with this (and Compose). With latest studio and up-to-date implementations I have a error I don't know how to fix.
In the construction of the ViewModel via the Factory, I received an error pinned on the "object" line which reads:
Inheritance from an interface with '@JvmDefault' members is only allowed with -Xjvm-default option
Have you seen this and do you have any suggestions on what I should do to fix it?
thanks,
steve

@Drjacky
Copy link

Drjacky commented Oct 21, 2022

@marenovakovic
Copy link
Author

@sralpert I will check it out

@marenovakovic
Copy link
Author

@sralpert sorry for late response. I can't reproduce that, no issues when I try to implement it. best is to follow that StackOverflow link

@sralpert
Copy link

sralpert commented Nov 8, 2022 via email

@PatricioIN
Copy link

PatricioIN commented Nov 8, 2022

@Drjacky sorry. Where did you put the line connectivityManager.requestNetwork(request, networkStatusCallback, 1000)? Thank you very much

@Drjacky
Copy link

Drjacky commented Nov 9, 2022

@PatricioIN

...
    val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()

        connectivityManager.registerNetworkCallback(request, callback)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            connectivityManager.requestNetwork(request, callback, 1000) //THISSSS
        }

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment