Repository: apollographql/apollo-kotlin (kotlin)

Issue:
Using `NetworkMonitor` to monitor offline state
### Use case

We'd love to use `NetworkMonitor` to monitor network state independently of GraphQL operations. Currently we are able to check the current state of the network and wait for the online state, but we are not able to wait for the offline state.

### Describe the solution you'd like

Would it be possible to add a `Flow<Boolean>` to the API? Or a `waitForOffline` suspending function?

Pull request:
Closes https://github.com/apollographql/apollo-kotlin/issues/6115

The previous interface was based on what `ApolloClient` needs but it prevented using `NetworkMonitor` for other use cases. This PR allows to use `NetworkMonitor` in a more generic way. 

This is obviously 200% breaking. If anyone implements their own `NetworkMonitor`, they'll need to update but hopefully it's not too hard (and the symbol is `@ApolloExperimental` in all cases).

Patch:
diff --git a/docs/source/advanced/network-connectivity.mdx b/docs/source/advanced/network-connectivity.mdx
index d15ec7a42f0..6109a8b20bb 100644
--- a/docs/source/advanced/network-connectivity.mdx
+++ b/docs/source/advanced/network-connectivity.mdx
@@ -27,6 +27,9 @@ val apolloClient = ApolloClient.Builder()
     .serverUrl("https://example.com/graphql")
     .retryOnErrorInterceptor(RetryOnErrorInterceptor(networkMonitor))
     .build()
+
+// once you're done with your `ApolloClient`
+networkMonitor.close()
 ```
 
 ### `failFastIfOffline`
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/interceptor/RetryOnErrorInterceptor.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/interceptor/RetryOnErrorInterceptor.kt
index b257a585773..9ed47b53504 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/interceptor/RetryOnErrorInterceptor.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/interceptor/RetryOnErrorInterceptor.kt
@@ -8,6 +8,7 @@ import com.apollographql.apollo.exception.ApolloException
 import com.apollographql.apollo.exception.ApolloNetworkException
 import com.apollographql.apollo.exception.OfflineException
 import com.apollographql.apollo.network.NetworkMonitor
+import com.apollographql.apollo.network.waitForNetwork
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.emitAll
@@ -60,7 +61,7 @@ private class DefaultRetryOnErrorInterceptorImpl(private val networkMonitor: Net
     val downStream = chain.proceed(request)
 
     return flow {
-          if (failFastIfOffline && networkMonitor?.isOnline() == false) {
+          if (failFastIfOffline && networkMonitor?.isOnline?.value == false) {
             emit((ApolloResponse.Builder(request.operation, request.requestUuid).exception(OfflineApolloException).build()))
           } else {
             emitAll(downStream)
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/NetworkMonitor.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/NetworkMonitor.kt
index c6f184b97d6..8555cef708c 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/NetworkMonitor.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/NetworkMonitor.kt
@@ -5,11 +5,14 @@ package com.apollographql.apollo.network
 
 import com.apollographql.apollo.annotations.ApolloExperimental
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.takeWhile
 import okio.Closeable
+import kotlin.js.JsName
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 
@@ -20,35 +23,35 @@ import kotlin.jvm.JvmName
 @ApolloExperimental
 interface NetworkMonitor : Closeable {
   /**
-   * The current state of the network
+   * Emits the current network state. May emit null during initialization
+   * when the current state is not known yet.
    */
-  suspend fun isOnline(): Boolean
+  val isOnline: StateFlow<Boolean?>
+}
 
-  /**
-   * Waits until [isOnline] is true
-   */
-  suspend fun waitForNetwork()
+internal suspend fun NetworkMonitor.waitForNetwork() {
+  isOnline.takeWhile { it != true }.collect()
 }
 
+/**
+ * @param networkObserverFactory a factory for a [NetworkObserver]. [networkObserverFactory] is called from a
+ * background thread.
+ */
 internal class DefaultNetworkMonitor(private val networkObserverFactory: () -> NetworkObserver) : NetworkMonitor, NetworkObserver.Listener {
   private val _isOnline: MutableStateFlow<Boolean?> = MutableStateFlow(null)
 
+  override val isOnline: StateFlow<Boolean?>
+    get() {
+      networkObserver
+      return _isOnline.asStateFlow()
+    }
+
   private val networkObserver by lazy {
     networkObserverFactory().also {
       it.setListener(this)
     }
   }
 
-  override suspend fun isOnline(): Boolean {
-    networkObserver
-    return _isOnline.mapNotNull { it }.first()
-  }
-
-  override suspend fun waitForNetwork() {
-    networkObserver
-    _isOnline.takeWhile { it != true }.collect()
-  }
-
   override fun close() {
     networkObserver.close()
   }


Test patch:
diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/network/NetworkMonitorTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/network/NetworkMonitorTest.kt
index c0b6fc84e7f..363c2db27b5 100644
--- a/libraries/apollo-runtime/src/commonTest/kotlin/test/network/NetworkMonitorTest.kt
+++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/network/NetworkMonitorTest.kt
@@ -13,11 +13,14 @@ import com.apollographql.mockserver.MockServer
 import com.apollographql.mockserver.assertNoRequest
 import com.apollographql.mockserver.enqueueString
 import com.apollographql.apollo.network.NetworkMonitor
+import com.apollographql.apollo.network.waitForNetwork
 import test.FooQuery
 import com.apollographql.apollo.testing.internal.runTest
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.emitAll
 import kotlinx.coroutines.flow.first
@@ -91,15 +94,10 @@ class NetworkMonitorTest {
 class FakeNetworkMonitor: NetworkMonitor {
   val _isOnline = MutableStateFlow(false)
 
-  override fun close() {}
-
-  override suspend fun waitForNetwork() {
-    _isOnline.takeWhile { !it }.collect()
-  }
+  override val isOnline: StateFlow<Boolean?>
+    get() = _isOnline.asStateFlow()
 
-  override suspend fun isOnline(): Boolean {
-    return _isOnline.mapNotNull { it }.first()
-  }
+  override fun close() {}
 }
 
 class NetworkMonitorInterceptor(private val networkMonitor: NetworkMonitor): ApolloInterceptor {