Playground sample
Given this code, the exception thrown in getRecords() is not caught in testFlattenMerge() - should it not be catchable though? Also, in getPeople(), the exception can be caught which causes flattenMerge() to work as expected, but it prints out "Caught in people" before any numbers are printed, and not after 64, as I expected. Is this the correct behavior? I can't quite fit my mental model of flattenMerge() around this.
import kotlin.reflect.KProperty
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
fun getRecords(id: Int) = flow {
repeat(5) { emit("A record for $id") }
if (id == 6) throw RuntimeException("Anything but #6!")
repeat(5) { emit("A record for $id") }
}
fun getPeople() = flow {
repeat(10) { emit(getRecords(it)) }
// repeat(10) { emit(getRecords(it).catch{ println("Caught in getPeople()")}) } // This works, but it prints /before/ any cnt lines...?
}
suspend fun testFlattenMerge() {
println ("Merge with flattenMerge()")
var cnt = 0
val flowOfFlows = getPeople()
flowOfFlows.catch{ println("Caught before flattenMerge")}
.flattenMerge()
.catch{ println("Caught after flattenMerge")}
.collect {
println("${cnt++}") // Without catching inside getPeople() this stops at 64
}
}
suspend fun testManualMerge() {
println("Merge manually")
var cnt = 0
repeat(10) {
getRecords(it).catch{ println("Caught in manual merge") }
.collect {
println("${cnt++}") // This goes up to 94, as expected
}
}
}
fun main() = runBlocking {
testFlattenMerge()
testManualMerge()
}
Related
I have the code to monitor if internet is available. It returns a LiveData and it is observed in the MainActivity . The code is given below.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding=DataBindingUtil.setContentView(this,R.layout.activity_main)
NetworkStatusHelper(this#MainActivity).observe(this, Observer {
when(it){
NetworkStatus.Available-> Snackbar.make(binding.root, "Back online", Snackbar.LENGTH_LONG).show()
NetworkStatus.Unavailable-> Snackbar.make(binding.root, "No Internet connection", Snackbar.LENGTH_LONG).show()
}
})
}
NetworkHelper
package com.todo.utils.networkhelper
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.util.Log
import androidx.lifecycle.LiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
class NetworkStatusHelper(private val context: Context): LiveData<NetworkStatus>() {
var connectivityManager: ConnectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private lateinit var connectivityManagerCallback: ConnectivityManager.NetworkCallback
val validNetworkConnections: ArrayList<Network> = ArrayList()
fun getConnectivityCallbacks() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
val networkCapability =
connectivityManager.getNetworkCapabilities(network)
val hasNetworkConnection =
networkCapability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
?: false
if (hasNetworkConnection) {
determineInternetAccess(network)
}
}
override fun onLost(network: Network) {
super.onLost(network)
validNetworkConnections.remove(network)
announceNetworkStatus()
}
// override fun onCapabilitiesChanged(
// network: Network,
// networkCapabilities: NetworkCapabilities
// ) {
// super.onCapabilitiesChanged(network, networkCapabilities)
//
// Log.d("validNetworkConnection","onCapabilitiesChanged size "+validNetworkConnections.size)
//
//
// if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
// determineInternetAccess(network)
// } else {
// validNetworkConnections.remove(network)
// }
// announceNetworkStatus()
// }
private fun determineInternetAccess(network: Network) {
CoroutineScope(Dispatchers.IO).launch {
if (InternetAvailability.check()) {
withContext(Dispatchers.Main) {
validNetworkConnections.add(network)
announceNetworkStatus()
}
}
}
}
fun announceNetworkStatus() {
if (validNetworkConnections.isNotEmpty()) {
postValue(NetworkStatus.Available)
} else {
postValue(NetworkStatus.Unavailable)
}
}
}
} else {
TODO("VERSION.SDK_INT < LOLLIPOP")
}
override fun onActive() {
super.onActive()
connectivityManagerCallback = getConnectivityCallbacks()
val networkRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
NetworkRequest
.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
} else {
TODO("VERSION.SDK_INT < LOLLIPOP")
}
connectivityManager.registerNetworkCallback(networkRequest, connectivityManagerCallback)
}
override fun onInactive() {
super.onInactive()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
connectivityManager.unregisterNetworkCallback(connectivityManagerCallback)
}
}
object InternetAvailability {
fun check() : Boolean {
return try {
val socket = Socket()
socket.connect(InetSocketAddress("8.8.8.8",53))
socket.close()
true
} catch ( e: Exception){
e.printStackTrace()
false
}
}
}
}
The problem is here is , the Snackbar is displayed even when the app is opened for the first time .I don't want the Snackbar to be displayed when the app is opened for the first time when network is available. If network is unavailable, then the Snackbar should be displayed even when the app is opened for the first time.
Can someone help to improve the code with correct logic to implement the same.
If your helper class is a Flow, then you can use Flow operators to easily customize its behavior. You should keep the instance of your helper class in a ViewModel so it can maintain its state when there are configuration changes.
Here's a Flow version of your class's functionality. I actually just made it into a function, because I think that's simpler.
I removed the List<Network> but you can add it back in if you think it's necessary. I don't think it makes sense to keep a List that can only ever hold at most one item. A device cannot have multiple simultaneous network connections. If you do need it, it won't work for pre-Lollipop, so you will have to juggle differing functionality and probably do need a class instead of just a function.
I think you can probably remove the checkAvailability() function as it is redundant, but I put it in because you have it.
I added a pre-Lollipop version based on a broadcast receiver, since you seem to want to add support for that.
#get:RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
val Context.networkStatus: Flow<NetworkStatus> get() = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> getNetworkStatusLollipop(this)
else -> getNetworkStatusPreLollipop(this)
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
#RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
private fun getNetworkStatusLollipop(context: Context): Flow<NetworkStatus> = callbackFlow {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val callback = object : ConnectivityManager.NetworkCallback() {
private var availabilityCheckJob: Job? = null
override fun onUnavailable() {
availabilityCheckJob?.cancel()
trySend(NetworkStatus.Unavailable)
}
override fun onAvailable(network: Network) {
availabilityCheckJob = launch {
send(if(checkAvailability()) NetworkStatus.Available else NetworkStatus.Unavailable)
}
}
override fun onLost(network: Network) {
availabilityCheckJob?.cancel()
trySend(NetworkStatus.Unavailable)
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
#RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
private fun getNetworkStatusPreLollipop(context: Context): Flow<NetworkStatus> = callbackFlow {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val receiver = object: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
launch {
if (connectivityManager.activeNetworkInfo?.isConnectedOrConnecting == true) {
send(if(checkAvailability()) NetworkStatus.Available else NetworkStatus.Unavailable)
} else {
send(NetworkStatus.Unavailable)
}
}
}
}
context.registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
awaitClose { context.unregisterReceiver(receiver) }
}
private suspend fun checkAvailability() : Boolean = withContext(Dispatchers.IO) {
try {
Socket().use {
it.connect(InetSocketAddress("8.8.8.8", 53))
}
true
} catch (e: Exception){
e.printStackTrace()
false
}
}
Then in your ViewModel, you can use Flow operators to easily expose a Flow that skips initial NetworkStatus.Available values:
class MyViewModel(application: Application): AndroidViewModel(application) {
val changedNetworkStatus = application.context.networkStatus
.dropWhile { it == NetworkStatus.Available } // ignore initial available status
.shareIn(viewModelScope, SharingStarted.Eagerly, 1) // or .asLiveData() if preferred
}
As the title implies, I am curious if there's any difference between doing this;
fun main() {
val job = GlobalScope.launch(Dispatchers.Main) {
withTimeout(2000L) {
delayMe()
}
}
job.invokeOnCompletion { cause -> println("We were canceled due to $cause") }
}
suspend fun delayMe() {
withContext(Dispatchers.Default) {
delay(5000L)
}
}
or this;
fun main() {
GlobalScope.launch(Dispatchers.Main) {
try {
withTimeout(2000L) {
delayMe()
}
} catch(cause: Exception){
println("We were canceled due to $cause")
}
}
...
}
...
in terms of handling exceptions inside coroutines.
PS: The sample code above is inspired from here.
I need to run a task, which emits some data. I want to subscribe to this data like PublishSubject. But I can't solve a problem of one-instance flow. If I try to call it again, it will create another instance and the job will be done twice.
I tried to run the flow internally and post values to the BroadcastChannel, but this solution doesn't seem correct.
What is the best practice for such a task?
This will do the magic:
fun <T> Flow<T>.refCount(capacity: Int = Channel.CONFLATED, dispatcher: CoroutineDispatcher = Dispatchers.Default): Flow<T> {
class Context(var counter: Int) {
lateinit var job: Job
lateinit var channel: BroadcastChannel<T>
}
val context = Context(0)
fun lock() = synchronized(context) {
if (++context.counter > 1) {
return#synchronized
}
context.channel = BroadcastChannel(capacity)
context.job = GlobalScope.async(dispatcher) {
try {
collect { context.channel.offer(it) }
} catch (e: Exception) {
context.channel.close(e)
}
}
}
fun unlock() = synchronized(context) {
if (--context.counter == 0) {
context.job.cancel()
}
}
return flow {
lock()
try {
emitAll(context.channel.openSubscription())
} finally {
unlock()
}
}
}
Consider the following code:
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
val channel = Channel<String>()
launch {
channel.send("A1")
channel.send("A2")
log("A done")
}
launch {
channel.send("B1")
log("B done")
}
launch {
for (x in channel) {
log(x)
}
}
}
fun log(message: Any?) {
println("[${Thread.currentThread().name}] $message")
}
The original version has the receiver coroutine like that:
launch {
repeat(3) {
val x = channel.receive()
log(x)
}
}
It expects only 3 messages in the channel. If I change it to the first version then I need to close the channel after all producer coroutines are done. How can I do that?
A possible solution is to create a job that will wait for all channel.send() to finish, and call channel.close() in the invokeOnCompletion of this job:
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
val channel = Channel<String>()
launch {
launch {
channel.send("A1")
channel.send("A2")
log("A done")
}
launch {
channel.send("B1")
log("B done")
}
}.invokeOnCompletion {
channel.close()
}
launch {
for (x in channel) {
log(x)
}
}
}
fun log(message: Any?) {
println("[${Thread.currentThread().name}] $message")
}
I've subclassed DataInputStream and added little-endian methods. Observe that I add a new inp property that represents the original InputStream. I need to reference this property in the new methods. The following code works fine:
import java.io.*
import org.apache.poi.util.*
class MyDataInputStream(val inp: InputStream) : DataInputStream(inp) {
fun readShortLittle(): Short {
val r: Short
try {
r = LittleEndian.readShort(inp)
} catch (e: LittleEndian.BufferUnderrunException) {
throw EOFException()
}
return r
}
fun readIntLittle(): Int {
val r: Int
try {
r = LittleEndian.readInt(inp)
} catch (e: LittleEndian.BufferUnderrunException) {
throw EOFException()
}
return r
}
fun readLongLittle(): Long {
val r: Long
try {
r = LittleEndian.readLong(inp)
} catch (e: LittleEndian.BufferUnderrunException) {
throw EOFException()
}
return r
}
}
fun main(args: Array<String>) {
var i: Int
val inp = MyDataInputStream(System.`in`)
while (true) {
// llegir int en binari si EOF break
try {
i = inp.readIntLittle()
} catch (e: EOFException) {
break
}
println(i);
}
}
However I wonder how to do this with extension methods such as:
fun DataInputStream.readShortLittle(): Short {
...
}
fun DataInputStream.readIntLittle(): Int {
....
}
fun DataInputStream.readLongLittle(): Long {
....
}
I've problems when defining the new inp property.
Strictly speaking, you can't, because it's accessible by a protected field which isn't visible to an extension method. But using this instead of inp should work because LittleEndian.read methods will call read on the DataInputStream which will delegate to in.read.