I had the need in one of my activies to handle displaying a message to the user when the network disconnected, and then reload the data for a RecyclerView
adapter when the network reconnected to ensure it was up to date.
After getting that working for my use case, I wanted to make the implementation more abstract for reuse in other, future activities.
While this works as expected, I'm still relatively new to Android development. Is there any better/recommended way of doing this, or any areas of my solution that might be improved?
Here is my current implementation:
/**
* Base activity that provides methods to handle network connect and disconnect events, and to
* optionally display an error message Snackbar to the user on network disconnect.
*/
public abstract class NetworkAwareActivity extends AppCompatActivity {
private Snackbar networkErrorSnackbar;
private String errorMessageString;
private boolean errorMessageEnabled = false;
private boolean errorMessageDismissible = false;
private View errorMessageParentView;
/**
* Called when the network becomes connected from a disconnected state.
*/
protected abstract void onNetworkConnected();
/**
* Called when the network becomes disconnected from a connected state.
*/
protected abstract void onNetworkDisconnected();
/**
* This method should be called once in onCreate() to setup the error Snackbar.
* After this has been called, the error Snackbar will remain enabled for the life of this Activity.
* Not calling this method will only prevent the Snackbar from being displayed;
* the onNetworkConnected() and onNetworkDisconnected() methods will still work as expected.
* Subsequent calls to this method are ignored.
* @param message Message to display in Snackbar
* @param dismissible Whether the Snackbar should be dismissible
*/
public void enableErrorMessageSnackbar(String message, boolean dismissible) {
if (!errorMessageEnabled) {
errorMessageString = message;
errorMessageDismissible = dismissible;
errorMessageEnabled = true;
}
}
@Override
protected void onResume() {
super.onResume();
registerNetworkCheckReceiver();
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(networkStatusBroadcastReceiver);
}
/**
* Registers a BroadcastReceiver with filters for network state.
*/
private void registerNetworkCheckReceiver() {
IntentFilter networkStatusFilter = new IntentFilter();
networkStatusFilter.addAction("android.net.wifi.STATE_CHANGE");
networkStatusFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
registerReceiver(networkStatusBroadcastReceiver, networkStatusFilter);
}
/**
* BroadcastReceiver to handle changes in network state.
*/
private BroadcastReceiver networkStatusBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (NetworkUtil.isNetworkConnected(context)) {
onNetworkConnected();
hideErrorMessageSnackbar();
} else {
onNetworkDisconnected();
showErrorMessageSnackbar();
}
}
};
/**
* Shows the error message Snackbar, if enabled
*/
private void showErrorMessageSnackbar() {
if (errorMessageEnabled) {
initializeNetworkErrorSnackbar();
networkErrorSnackbar.show();
}
}
/**
* Hides the error message Snackbar if currently shown
*/
private void hideErrorMessageSnackbar() {
if (networkErrorSnackbar != null && networkErrorSnackbar.isShown()) {
networkErrorSnackbar.dismiss();
}
}
/**
* Creates an instance of the error Snackbar if none exists
*/
private void initializeNetworkErrorSnackbar() {
if (networkErrorSnackbar == null) {
networkErrorSnackbar = DesignUtil.makeErrorMessageSnackbar(getErrorMessageDisplayView(), errorMessageString, errorMessageDismissible);
}
}
/**
* Gets the view the Snackbar should be displayed in
* @return The root view of this activity
*/
private View getErrorMessageDisplayView() {
if (errorMessageParentView == null) {
errorMessageParentView = DesignUtil.getRootViewFromActivity(this);
}
return errorMessageParentView;
}
}
Utility class containing the isNetworkConnected
method:
/**
* Provides utility methods for networking.
*/
public class NetworkUtil {
/**
* Checks if the device is currently connected to the network.
* @param context Context this is being called from
* @return Whether the network state is connected or not
*/
public static boolean isNetworkConnected(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo currentNetwork = cm.getActiveNetworkInfo();
return currentNetwork != null && currentNetwork.isConnected();
}
}
Utility class containing the makeErrorMessageSnackbar
method:
/**
* Provides utility methods for working with user interface elements.
*/
public class DesignUtil {
/**
* Creates a Snackbar for displaying error messages with red background, white text, and indefinite display length.
* @param view View to create the Snackbar within
* @param message Message to display
* @param dismissible Whether the Snackbar should be dismissible or not
* @return An error Snackbar with the provided message
*/
public static Snackbar makeErrorMessageSnackbar(View view, String message, boolean dismissible) {
Snackbar errorMessageSnackbar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE);
View snackbarView = errorMessageSnackbar.getView();
snackbarView.setBackgroundColor(Color.RED);
TextView textView = (TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text);
textView.setTextColor(Color.WHITE);
if (dismissible) {
errorMessageSnackbar.setAction("X", new View.OnClickListener() {
@Override
public void onClick(View view) {
errorMessageSnackbar.dismiss();
}
});
errorMessageSnackbar.setActionTextColor(Color.WHITE);
}
return errorMessageSnackbar;
}
/**
* Gets the root view from an activity.
* @param activity Activity to get root view from
* @return The root view from the given activity
*/
public static View getRootViewFromActivity(Activity activity) {
return activity.getWindow().getDecorView().findViewById(android.R.id.content);
}
}
Example usage for a simple application fetching data from The Movie Database:
@Override
protected void onNetworkConnected() {
MovieDatabaseMovieAPI movieService = MovieDatabaseClientFactory.makeMovieDatabaseClient();
Call<MoviesResponse> call = movieService.getPopularMovies();
call.enqueue(new Callback<MoviesResponse>() {
@Override
public void onResponse(Call<MoviesResponse> call, Response<MoviesResponse> response) {
List<Movie> movies = response.body().results;
Timber.d("Got %d movies!", movies.size());
mRecyclerAdapter.replaceAll(movies);
}
@Override
public void onFailure(Call<MoviesResponse> call, Throwable t) {
if (t instanceof IOException) {
Toast.makeText(getApplicationContext(), "Failed to fetch movies. Try again later.", Toast.LENGTH_LONG).show();
}
Timber.e(t.toString());
}
});
}
EDIT:
I cleaned up the activity code a little and switched to having a single method (enableErrorMessageSnackbar()
) for setting up the Snackbar, to be called in onCreate()
. This approach makes the usage slightly less error prone, as no changes to the Snackbar itself should occur outside on onCreate()
. Now, if the user does not explicitly enable the Snackbar by calling this method, it will not be shown or initialized. This allows the user to have an activity that is network aware (providing onNetworkConnected()
and onNetworkDisconnected()
), without forcing them to 'opt-out' of having an error message shown.
Also, the default message is removed, forcing the user to provide one. This is more idiomatic and allows defining the string in a resource file, as it should be.
BroadcastReceiver
in that class, and that receiver is listening for network change events. \$\endgroup\$