4
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Be careful that opening the notification bar will invoke onPause (I think, my android lifecycle is a bit rusty), if I am correct, You could miss an event. For the design, it looks like what I would have done. Simple and flexible. \$\endgroup\$ – AxelH Sep 13 '16 at 13:08
  • \$\begingroup\$ @PseudoPsyche you haven't mentioned how those two abstract methods get called! It would be essential to give you a proper feedback \$\endgroup\$ – Jabbar_Jigariyo Nov 14 '16 at 16:32
  • \$\begingroup\$ @Jabbar_Jigariyo they are both being called within the BroadcastReceiver in that class, and that receiver is listening for network change events. \$\endgroup\$ – PseudoPsyche Nov 14 '16 at 17:21
2
\$\begingroup\$

The only problem I'm foreseeing is that, you can't implement this solution in Fragment or View, and thus you'll have pass the action from particular Activity to its Fragment or View. I would just create a similar kind of design without an Activity in some kind of singleton where it would maintain a list of listeners who are listening to this network changes!

You can then add/remove listeners from Activity/Fragment or even a View, and thus you are not restricted to having just child activity implementation.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.