5

So I'm looking into the feasibility of changing from callback interfaces to local broadcasts for some long-running network operations. Since the Activity lifecycle creates all sorts of complication for asynchronous requests that need to modify the UI (disconnect the Activity from the callback in onDestroy(), don't modify FragmentTransactions after onSaveInstanceState(), etc.), I'm thinking that using local broadcasts makes more sense, and we can just register/unregister the receiver at the lifecycle events.

However, when the Activity is destroyed and recreated during a configuration change, there's this small window of time when the broadcast receiver would not be registered (in between onPause()/onResume() for example). So if, for example, we start an asynchronous request in onCreate() if savedInstanceState == null (e.g. for the first launch of the Activity), isn't it possible that the broadcast sent upon completion would be lost if the user changes their device orientation right before the operation completes? (i.e. the receiver is unregistered on onPause(), then the operation completes, then the receiver is re-registered in onResume())

If that's the case, then it adds a lot of extra complexity we would need to add support for, and it's probably not worth the switch. I've looked into other things such as the Otto EventBus library but I'm not sure whether or not it has the same concerns to worry about.

Kevin Coppock
  • 133,643
  • 45
  • 263
  • 274
  • 2
    Android disables the message queue processing in the main thread while the activity is being restarted, so the broadcast should always be delivered after you have registered for it in one of the `Activity` lifecycle starting callbacks. – corsair992 Feb 26 '14 at 21:04
  • That sounds interesting, and I thought something like that may happen, but do you have a source on that? – Kevin Coppock Feb 26 '14 at 21:06
  • 2
    See this [comment](https://groups.google.com/d/msg/android-developers/4dW4-KMUKJI/Ar5ZcWTZtqMJ) by Dianne Hackborn. This is also documented in the `Activity` [`onRetainNonConfigurationInstance()`](http://developer.android.com/reference/android/app/Activity.html#onRetainNonConfigurationInstance()) method. – corsair992 Feb 26 '14 at 21:11
  • @corsair992 Thank you! This is insanely useful information. Unfortunately `onRetainNonConfigurationInstance()` is now deprecated in favor of Fragment `setRetainInstance()`, so I may post on that group and see if there's an equivalent guarantee for the Fragment lifecycle. If you want to post this information as an answer, I'll accept it. :) – Kevin Coppock Feb 26 '14 at 21:37
  • As the `Fragment` lifecycle is managed by it's `Activity`, I don't think it has any different semantics in this regard :) – corsair992 Feb 26 '14 at 21:55
  • The docs for `onRetainNonConfigurationInstance()` say it will be called between `onStop()` and `onDestroy()`. So it would seem that the asker's concern is still valid between the calls to `onPause()` and `onRetainNonConfigurationInstance()`. If that's correct, is it recommended to unregister broadcast receivers in `onPause()` if `isFinishing()` and in `onDestroy()` if not? – Kevin Krumwiede Jul 24 '14 at 20:58

4 Answers4

3

As documented in the onRetainNonConfigurationInstance() method of the Activity, the system disables the message queue processing in the main thread while the Activity is in the process of being restarted. This ensures that events posted to the main thread will always be delivered at a stable point in the lifecycle of the Activity.

However, there seems to be a design flaw in the sendBroadcast() method of LocalBroadcastManager, in that it evaluates the registered BroadcastReceivers from the posting thread before queuing the broadcast to be delivered on the main thread, instead of evaluating them on the main thread at the time of broadcast delivery. While this enables it to report the success or failure of the delivery, it does not provide the proper semantics to allow BroadcastReceivers to be safely unregistered temporarily from the main thread without the possibility of losing potential broadcasts.

The solution to this is to use a Handler to post the broadcasts from the main thread, using the sendBroadcastSync() method so that the broadcasts are delivered immediately instead of being reposted. Here's a sample utility class implementing this:

public class LocalBroadcastUtils extends Handler {
    private final LocalBroadcastManager manager;

    private LocalBroadcastUtils(Context context) {
        super(context.getMainLooper());
        manager = LocalBroadcastManager.getInstance(context);
    }

    @Override
    public void handleMessage(Message msg) {
        manager.sendBroadcastSync((Intent) msg.obj);
    }

    private static LocalBroadcastUtils instance;

    public static void sendBroadcast(Context context, Intent intent) {
        if (Looper.myLooper() == context.getMainLooper()) {
            // If this is called from the main thread, we can retain the
            // "optimization" provided by the LocalBroadcastManager semantics.
            // Or we could just revert to evaluating matching BroadcastReceivers
            // at the time of delivery consistently for all cases.
            LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
        } else {
            synchronized (LocalBroadcastUtils.class) {
                if (instance == null) {
                    instance = new LocalBroadcastUtils(context);
                }
            }
            instance.sendMessage(instance.obtainMessage(0, intent));
        }
    }
}
corsair992
  • 3,050
  • 22
  • 33
  • For future viewers, actually this is not a solution for asked onPause()/onResume() use case. This leaves gaps between onPause() and onDestroy() calls as well as between onCreate() and onResume() calls. – biegleux Nov 12 '14 at 01:22
  • @biegleux: Assuming that the destruction lifecycle will be performed sequentially, there should not be any "gaps" for broadcast delivery. – corsair992 Nov 12 '14 at 12:35
  • Documentation in onRetainNonConfigurationInstance() deal with another use case. It tell nothing about onPause() / onResume() calls. If you send broadcast in onStop() while you have unregistered the receiver in onPause() you'll see that the broadcast won't be delivered, although lifecycle methods were invoked by orientation change and the receiver is registered in onResume()'s method of the new activity. Same applies if you send broadcast in onCreate() method of the new activity. – biegleux Nov 12 '14 at 18:38
  • @biegleux: If you post a `Message` on a `Handler` in the `onPause()` callback, and then recreate the `Activity`, you will see that it will be delivered _after_ the `Activity` is recreated and all the destruction and creation callbacks have been performed. However, you are correct that `LocalBroadcastManager` will not post any broadcast if there is no `BroadcastReceiver` registered to it, and in fact will only deliver broadcasts to those `BroadcastReceiver`s that are registered at the time of posting the broadcast. I have edited my answer to add that, and suggested a solution to this issue. – corsair992 Nov 12 '14 at 22:19
2

To overcome this issue you need a component which stays alive even when activity gets re-created on configuration change. You can either use Application singleton or a retained Fragment.

If you use Otto or EventBus, then you can create an instance of event bus as a field of Application, and it will stay decoupled from device configuration changes like orientation change. Your activity will need to register event listener in onStart() and it will receive latest events.

If you use a retained Fragment, then fragment will stay alive until activity is not finished. Configuration changes will not release the instance of retained fragment either. It is also good practice to make retained Fragment invisible (return null from onCreateView() method). In onStart() of your activity you can always puck up latest state from that Fragment.

You can use LocalBroadcastManager with one of these approaches, but it doesn't really addresses the issue. It's just like any other event bus, but with ugly and inconvenient API ;)

sergej shafarenka
  • 20,071
  • 7
  • 67
  • 86
1

I found android loaders is extremly helpful in this case.

In my case I need to receive broadcasts from another application and manage fragment transitions in my application.

So i did like below.

/**
 * LoaderManager callbacks
 */
private LoaderManager.LoaderCallbacks<Intent> mLoaderCallbacks =
        new LoaderManager.LoaderCallbacks<Intent>() {

            @Override
            public Loader<Intent> onCreateLoader(int id, Bundle args) {
                Logger.v(SUB_TAG + " onCreateLoader");
                return new MyLoader(MyActivity.this);
            }

            @Override
            public void onLoadFinished(Loader<Intent> loader, Intent intent) {
                Logger.i(SUB_TAG + " onLoadFinished");
                // Display our data
                if (intent.getAction().equals(INTENT_CHANGE_SCREEN)) {
                    if (false == isFinishing()) {
                        // handle fragment transaction 
                        handleChangeScreen(intent.getExtras());

                    }
                } else if (intent.getAction().equals(INTENT_CLOSE_SCREEN)) {
                    finishActivity();
                }
            }

            @Override
            public void onLoaderReset(Loader<Intent> loader) {
                Logger.i(SUB_TAG + " onLoaderReset");
            }
};

 /**
 * Listening to change screen commands. We use Loader here because
 * it works well with activity life cycle.
 * eg, like when the activity is paused and we receive command, it
 * will be delivered to activity only after activity comes back.  
 * LoaderManager handles this.
 */
private static class MyLoader extends Loader<Intent> {

    private Intent mIntent;
    BroadcastReceiver mCommadListner;
    public MyLoader(Context context) {
        super(context);
        Logger.i(SUB_TAG + " MyLoader");
    }

    private void registerMyListner() {
        if (mCommadListner !=  null) {
            return;
        }
        Logger.i(SUB_TAG + " registerMyListner");
        mCommadListner = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (action == null || action.isEmpty()) {
                    Logger.i(SUB_TAG + " intent action null/empty returning: ");
                    return;
                }
                Logger.i(SUB_TAG + " intent action: " + action);
                mIntent = intent;
                deliverResult(mIntent);
            }
        };
        IntentFilter filter = new IntentFilter();
        filter.addAction(INTENT_CHANGE_SCREEN);
        getContext().registerReceiver(mCommadListner, filter);
    }

    @Override
    protected void onStartLoading() {
        Logger.i(SUB_TAG + " onStartLoading");
        if (mIntent != null) {
            deliverResult(mIntent);
        }
        registerMyListner();
    }

    @Override
    protected void onReset() {
        Logger.i(SUB_TAG + "Loader onReset");
        if (mCommadListner != null) {
            getContext().unregisterReceiver(mCommadListner);
            mCommadListner = null;
        }
    }
}

Activity#onCreate or Fragment@onActivityCreated()
@Override
protected void onCreate(Bundle savedInstanceState) {
    // Listening to change screen commands from broadcast listner. We use Loader here because
    // it works well with activity life cycle.
    // eg, like when the activity is paused and we receive intent from broadcast, it will delivered
    // to activity only after activity comes back. LoaderManager handles this.
    getLoaderManager().initLoader(0, null, mLoaderCallbacks);
}
Deepu
  • 598
  • 6
  • 12
0

Normal broadcast will be lost if your activity will be paused or recreated. You can use sticky broadcast but it doesn't work with LocalBroadcastManager and you have to remember to manually remove sticky broadcast by calling Context.removeStickyBroadcast(). Sticky broadcast will be kept by system(even if your activity is paused) until you decide to delete it.

EventBus offer postSticky() method which works similar to sticky broadcast.

Leszek
  • 6,568
  • 3
  • 42
  • 53