How to Monitor/Observe Network Availability for Android Projects

Network/Cellular data is widely used in Android Projects. Most of the app currently exist in the play store have API call some way or another. As in the nature of API (Application Programming Interface), you are interacting with another application that your project does not have any authority in the codebase. Sometimes, these APIs fail. And, when they fail, it is very important to show proper error message to the customers about what went wrong. Otherwise, the users will think that your app is not working properly.

One common problem that API might be failing due to the network availability. In case there is no cellular connection available in the user’s device, the API might fail. Since we as developers know that it does not make sense to use resources to launch a network call when there is no cellular/wifi connection available, a message can be prompted to the users regarding to the network availability. Thus, the customers can try to find a network connection and then start using your application.

In this article, we will try to cover how we can monitor, observe, or collect the network availability so that the user will not need to re-launch your app when they get a good connection. We will use ConnectivityManager service, which is already provided in Android SDK.

What is ConnectivityManager?

ConnectivityManager is a system service in Android platform. It is responsible to answer queries about network connectivity. ConnectivityManager allows us to monitor connectivity changes via using callbacks or Broadcast intents, and gives us specific details on the connection type (Cellular, WiFi, etc.). Also, it provides an API that allows applications request and select networks for their data traffic.

You can retrieve ConnectivityManager like;

class MyActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
  }
}
Kotlin
class MyActivity extends AppCompatActivity {

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
  }
}
Java

1. Introducing Network Callback

The callback mechanism is a great way to observe network availability. ConnectivityManager provides a clean architecture to implement it. Meet with NetworkCallbacks. This class can be imagined as the lifecycle of the network connectivity. It allows us to know when the connectivity is available, when it is lost, and when a type of connectivity is changed. Some basic functions are down below. Please, visit at developer.android.com website for more details.

voidonAvailable(Network network)

Called when the framework connects and has declared a new network ready for use.
voidonCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities)

Called when the network corresponding to this request changes capabilities but still satisfies the requested criteria.
voidonLosing(Network network, int maxMsToLive)

Called when the network is about to be lost, typically because there are no outstanding requests left for it.
voidonLost(Network network)

Called when a network disconnects or otherwise no longer satisfies this request or callback.
voidonUnavailable()

Called if no network is found within the timeout time specified in ConnectivityManager#requestNetwork call or if the requested network request cannot be fulfilled (whether or not a timeout was specified).

2. Creating Network Request (< API 24)

Network request establishes the contract on how and when we want to be notified regarding to connectivity changes. Network request allows us to set transport types that we are interested in observing. Note that this is not required API level 24 and above.

An example of how we can create network request.

class MyActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
    val networkRequest = NetworkRequest.Builder()
        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
        .build()
  }
}
Kotlin
class MyActivity extends AppCompatActivity {

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkRequest networkRequest = new NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .build();
  }
}
Java

3. Observing Network Availability

This final step will combine everything we learned so far. Now, observing network availability is much easier using network callbacks and network request. To do that, we need to wrap our NetworkCallbacks with Flow, or Observable depending on the preferred library. The code should look like below;

class ConnectivityStateCollector(
  private val connectivityManager: ConnectivityManager,
  private val networkRequest: NetworkRequest
) {

  val connectivityFlow: Flow<Boolean> by lazy {
    callbackFlow { 
        object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                trySend(true)
            }
            override fun onLost(network: Network) {
                trySend(false)
            }
        }.let { callback ->
            connectivityManager.requestNetwork(networkRequest, callback)
            awaitClose { 
                connectivityManager.unregisterNetworkCallback(callback)
            }
        }
    }
  }
}
Kotlin
class ConnectivityStateObserver {

  private ConnectivityManager connectivityManager;
  private NetworkRequest networkRequest;
  
  ...
  
  public Observable<Boolean> connectivityObservable() {
    return return Observable.create(emitter -> {
      ConnectivityManager.NetworkCallback callback = new ConnectivityManager.NetworkCallback() {
      
        @Override
        public void onAvailable(Network network) {
          emitter.onNext(true);
        }
        
        @Override
        public void onLost(@NonNull Network network) {
          emitter.onNext(false);
        }
      };
      connectivityManager.requestNetwork(networkRequest, callback);
      emitter.setCancellable(() -> connectivityManager.unregisterNetworkCallback(callback));
    }
  }
}
Java

Ok. It might be quite a lot above. But let us break into smaller steps. First we create observable chain. (It is either callbackFlow or Observable.create respectively). Within the lambda function, we create our Network Callback instance, which will provide us network availability callbacks. Next big thing we need to consider is that, each callback we receive, we need to emit new item in our observing channel. We emit true when onAvailable is called, and false when onLost is called.

Now, we need to register our network request to ConnectivityManager. We can achieve that through ConnectivityManager#requestNetwork. All there is left to pass both our networkRequest and callback instances we created above. Notice that we are still in the lambda function of Flow or Observable constructor chain respectively.

Finally, as every environmental programmer should, we need to clear unused object when not needed. In this case, if collector/subscriber cancels or disposes the process, then we need to unregister the network callback so that we would not cause any memory leaks (For more info in memory leak, see this article). We can achieve that using ConnectivityManager#unregisterNetworkCallback, and we need to pass our networkCallback reference.

Please see more on default network request for API level 24+ in this article.

Now all there is to use connectivity state in our ViewModel. More than likely, the business logic will be not to call repository if there is no network available. So, the logic should look like;

class MyViewModel(
  private val repository: Repository,
  private val connectivityState: ConnectivityStateCollector
) : ViewModel() {

  val uiState by lazy {
    connectivityState.connectivityFlow
      .flatMapLatest { isConnected ->
        if (isConnected) repository.fetchUiState()
        else flowOf(NetworkLostUiState)
      }
      .stateIn(viewModelScope, SharingStarted.Eagerly, null)
  }
}
Kotlin
class MyViewModel extends ViewModel {

  private Repository repository;
  private ConnectivityStateObserver connectivityState;
  
  public Observable<UiState> listenUiState() {
    return connectivityState.connectivityObservable()
      .switchMap(isConnected -> {
        if (isConnected) repository.fetchUiState()
        else Observable.just(NetworkLostUiState)
      })
  }
}
Java

4. Bonus

You made it this far. For my readers speciality, I want to give you guys some bonus tips and tricks. If you test above code in a sample application, you should realize that connectivityState will be keep changing true to false when you turn on/off the WiFi or cellular data. This is great, that is actually what we wanted. However, did you test on opening the app without any network availability?

If you did so, you should realized that there would not be any emission. NetworkCallback will not call onLost if there was no network in the first place. Well, it makes sense, but what if your UI depends on this emission?

So, first and easier solution you can do is that, instead of null default state in your UI, you can have initial UI state as NetworkLostUiState. If there is no emission, your UI will show the correct state, instead of drawing nothing. Or, second best thing you can do is to use functional programming approach to have the first emission if there is no network available at the launch.

The logic should look like;

class ConnectivityStateCollector(...) {

  val connectivityFlow: Flow<Boolean> by lazy {
    callbackFlow { 
        ...
    }
    .onStart { if (isConnected().not()) emit(false) }
  }
  
  private fun isConnected(): Boolean = connectivityManager.activeNetwork?.run {
        connectivityManager.getNetworkCapabilities(this)?.let { networkCapabilities ->
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
                networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
        } ?: false
    } ?: false
}
Kotlin
class ConnectivityStateObserver {

  private ConnectivityManager connectivityManager;
  private NetworkRequest networkRequest;
  
  ...
  
  public Observable<Boolean> connectivityObservable() {
    return return Observable.create(emitter -> {
      ...
    }
    .startWith(observer -> {
      if (!isConnected()) observer.onNext(false);
      observer.onComplete();
    })
  }
  
  private boolean isConnected() {
    Network network = connectivityManager.getActiveNetwork();
    if (network == null) return false;
    NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
    if (capabilities == null) return false;
    return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
                || capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL);
  } 
}
Java

As you can see above, first we check if there is network available. If so, there should be emission coming up. Thus, if we send an emission here, it would be duplicated. However, if there is no network already, we start with false emission so that we can provide the network not available even though there was no network to even start with.

Conclusion

In this article, we learn how to observe network availability. Now, we can listen any network changes and react and update the UI properly to those changes immediately so that the users will not need to take any action.

More posts are coming. Until then, stay tuned, and happy coding …

Leave a Reply

Your email address will not be published. Required fields are marked *