17

I'm developing social app. Let's assume I have a stack of activities A -> B -> C -> D. D is in foreground and user presses "like" button to something there (post, comment, user etc.) What is the best way to notify all other activities about this action in order to refresh their data? I see 3 options here:

  1. Use local database and some loaders to automatically refresh the data. However, it requires a lot of code if we have different data-models with shared data (for instance BasicUserInfo, UserInfo, DetailedUserInfo).
  2. Use EventBus with sticky events (producers for Otto). In this case I must notify ONLY backstack activities and ignore those that will be created. Also I have to manage events overriding.
  3. Use a simple observer pattern with WeakReferences to backstack activities. But then I have a problem with killed activities that are going to be re-instantiated.

Real example:

In Instagram: I open some specific user's profile (A), there I open some specific post (B), again profile (A) and so on A -> B -> A -> B -> A .... So it loads data from the web everytime. On the step "n+1" a new comment to the post appears. If I start going back through my backstack I will see that instagram has dispatched this "new" comment to all B activities without reloading any data from web. So I'm interesting how do they do it.

user1049280
  • 5,176
  • 8
  • 35
  • 52
  • I Think Otto is best in case. – Jithin Sunny Oct 06 '15 at 08:24
  • @Sunny, Because activities are not registred when they are in background I have to use producers to make events sticky. And in this case I'm not sure how to deliver this events to backstack activities only. – user1049280 Oct 06 '15 at 08:33

7 Answers7

5

The main use case for a notification system (event, observer, BroadcastReceiver, ...) is when you want the recipient to act more or less immediately when something happens.

I think this is not the case here : the backstack activities don't need to act immediately as they are not visible. Besides they may even not exist anymore (killed / frozen). What they actually need is to get the latest data when they are back on the foreground (possibly after having been recreated).

Why not simply trigger the refresh in onStart() or onResume() (using a Loader or anything you already use) ?
If the 'liked' status needs to be persisted you could do it in D's onPause(). If not, the liked object could be stored in a global variable (which is actually what a sticky event is)

bwt
  • 17,292
  • 1
  • 42
  • 60
  • That's a good point. But what about new activities? Suppose situation that user opens another `A` from `D`: `A -> B -> C -> D -> A`. This new activity will load its own latest data from web and it doesn't need to know about any actions that've happened in `D`. So I must divide these `A` activities` `onStart()` logic in someway. – user1049280 Oct 08 '15 at 09:42
  • So you want the older `A` instance to have the latest data (updated in `D`), while the newer `A` instance should only have the old data (without the `D` updates) ? Interesting. You could timestamp / sequence both `D` updates and `A` creation to allow each instance to (re)load only the relevants parts. In case of kill / recreation cycles, activities sequence should be saved in `onSaveInstanceState()` to be retrieved in `onCreate()` – bwt Oct 08 '15 at 10:27
  • The newer `A` loads its data from web so it already has `D` updates (which were synchronized with server) and probably some updates from other users. Anyway your advice about timestamps will work. But I'm wondering are all those social apps do it in the same way? I think there should be some general approach which is simplier. – user1049280 Oct 08 '15 at 11:23
  • Then each activity can do a "check if something is new and reload" in the `onResume()`, it doesn't have to know about other activities. This will also cover the case when the user switch to another application then back to your's for example. In case of HTTP there are quite a few library that can handle that for you, provided the server use correctly the headers related to modification (`If-Modified-Since`, `Last-Modified`, `Cache-Control`, ...), – bwt Oct 08 '15 at 13:04
  • for example in Instagram: I open some specific user's profile (A) and there I open some specific post (B) and so on `A -> B -> A -> B -> A ...`. So it loads data from the web everytime. On the step "n+1" a new comment to the post appears. If I start going back through my backstack I will see that instagram has dispatched this "new" comment to all `B` activities without reloading any data from web. So I'm interesting how do they do it. – user1049280 Oct 08 '15 at 13:35
  • Some library have a 'local cache only' mode. Anyway I too would like to know what they actually do. – bwt Oct 08 '15 at 14:18
  • I think it keeps all data models in one place (database or just signletone container). But there should be some tracking mechanism to delete data that is no longer needed otherwise it would be a memory leak. – user1049280 Oct 08 '15 at 14:36
  • Maybe a [LruCache](https://developer.android.com/reference/android/util/LruCache.html) ? – bwt Oct 08 '15 at 14:43
  • perhaps, but I think it's a critical data and lru mechanism is not realible enough. I'm thinking about some reference-counting approach, some kind of garbage collector. – user1049280 Oct 08 '15 at 15:55
  • I was thinking about lru and probably you're right, case when user doesn't see some updates is undesirable but not critical. Let's see if there will be any other suggestions – user1049280 Oct 08 '15 at 16:27
3

You can use LocalBroadcastManager to notify your stacked activities that your Liked event has occured

Suppose in your Activity D:

private void liked() {
  Log.d("liked", "Broadcasting message");
  Intent intent = new Intent("like-event");
  // You can also include some extra data.
  intent.putExtra("message", "my like event occurs!");
  LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}

Now it will notify all the activities who are registered with this broadcast reciever

for example in your Activity A,B,C :

@Override
public void onCreate(Bundle savedInstanceState) {

  ...

  // Register to receive messages.
  // We are registering an observer (mMessageReceiver) to receive Intents
  // with actions named "custom-event-name".
  LocalBroadcastManager.getInstance(this).registerReceiver(mLikeEventReceiver ,
      new IntentFilter("like-event"));
}

// Our handler for received Intents. This will be called whenever an Intent
// with an action named "like-event" is broadcasted.
private BroadcastReceiver mLikeEventReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    // Get extra data included in the Intent
    String message = intent.getStringExtra("message");
    Log.d("receiver", "Got message: " + message);
  }
};

@Override
protected void onDestroy() {
  // Unregister since the activity is about to be closed.
  LocalBroadcastManager.getInstance(this).unregisterReceiver(mLikeEventReceiver );
  super.onDestroy();
}

references : [http://developer.android.com/reference/android/support/v4/content/LocalBroadcastManager.html][1] [how to use LocalBroadcastManager? [https://androidcookbook.com/Recipe.seam?recipeId=4547][3]

  1. [1]: http://developer.android.com/reference/android/support/v4/content/LocalBroadcastManager.html

  2. [2]: how to use LocalBroadcastManager?

  3. [3]: https://androidcookbook.com/Recipe.seam?recipeId=4547

Community
  • 1
  • 1
1

The classic way of handling this type of thing is to use BroadcastReceivers.

Here's an example receiver:

public class StuffHappenedBroadcastReciever extends BroadcastReceiver {


    private static final String ACTION_STUFF_HAPPENED = "stuff happened";
    private final StuffHappenedListener stuffHappenedListener;

    public StuffHappenedBroadcastReciever(@NonNull Context context, @NonNull StuffHappenedListener stuffHappenedListener) {

        this.stuffHappenedListener = stuffHappenedListener;
        context.registerReceiver(this, new IntentFilter(ACTION_STUFF_HAPPENED));
    }

    public static void notifyStuffHappened(Context context, Bundle data) {

        Intent intent = new Intent(ACTION_STUFF_HAPPENED);
        intent.putExtras(data);

        context.sendBroadcast(intent);

    }

    @Override
    public void onReceive(Context context, Intent intent) {

        stuffHappenedListener.onStuffHappened(intent.getExtras());

    }


    public interface StuffHappenedListener {

        void onStuffHappened(Bundle extras);
    }
}

And how to attach it to an activity:

public class MainActivity extends AppCompatActivity implements StuffHappenedBroadcastReciever.StuffHappenedListener {

    private StuffHappenedBroadcastReciever mStuffHappenedReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mStuffHappenedReceiver = new StuffHappenedBroadcastReciever(this, this);
    }

    @Override
    protected void onDestroy() {
        unregisterReceiver(mStuffHappenedReceiver);

        super.onDestroy();
    }

    @Override
    public void onStuffHappened(Bundle extras) {
        // do stuff here
    }
}

"onStuffHappened" will get called as long as the activity is alive.

JoeyJubb
  • 2,341
  • 1
  • 12
  • 19
  • basically, it's the same approach as using EventBus or Otto. What if one of my backstack activities were temprorary killed to save memory? Then it won't be registered to broadcast receiver. – user1049280 Oct 08 '15 at 09:26
  • If your activity was destroyed, then a new instance will be created and onCreate will happen within that instance -- just as if you're starting a new activity from scratch you should just load the most recent data. – JoeyJubb Oct 08 '15 at 10:07
  • As for me activity in backstack must keep it's state (even if it was re-instantiated) and loading the data again will work but it's a workaround – user1049280 Oct 08 '15 at 11:30
1

I agree with @bwt 's approach. Those notification systems should matter when you want to notify something immediately.

My approach would be a cache system. You don't need to deal with "if the activity is back stacked or newly created", you always need to query what you need in the onResume of the activity. So you will always get the most up to date data.

While fetching data from your server, you also need a copy of your data models in a local database. Before you ping your server when, e.g. there is a like in a user post, you need to set this as a Flag in local database, and send your Http Request to your server to say "Hey this post is liked". And later when you get the response from this request, if it's successful or not, attempt to modify this Flag again.

So in your Activities, you will get most up to date data if you Query from local database in onResume. And for changes in other users' post, you can have a BroadcastReceiver to reflect the changes in your Visible activity.

P.S: You can check Realm.io for relatively faster queries, since you will need to have local database calls quicker.

osayilgan
  • 5,873
  • 7
  • 47
  • 68
1

As a lot of other answers have eluded to, this is a classic example where a BroadcastReceiver would easily get the job done.

I would also recommend using the LocalBroadcastManager class in conjunction with BroadcastReceiver. From the documentation of BroadcastReceiver:

If you don't need to send broadcasts across applications, consider using this class with LocalBroadcastManager instead of the more general facilities described below. This will give you a much more efficient implementation (no cross-process communication needed) and allow you to avoid thinking about any security issues related to other applications being able to receive or send your broadcasts.

vdelricco
  • 749
  • 4
  • 15
  • I must register and unregister receivers on some step of activities lifecycle. Even if I use unsafe onCreate/onDestroy it means that temprorary killed activities' won't get broadcasted information – user1049280 Oct 14 '15 at 09:08
1

in order to refresh their data?

Here lies the answer. Activities should not own data. Activities present data and allow user to act upon it. Data itself should be in a separate entity that can be interacted with by activity instances, a Model.

Further, it should not be assumed that there is always an activity instance in back-stack. Those activities can be destroyed and later re-created as different objects by system when user navigates back. Data is always refreshed when the whole activity is being created.

Separate out data handling to specialized classes, that can be accessed by activities easily, and can provide data / event binding to activities on demand. A bound Service is a good candidate.

As far as not loading data from web is concerned, you can setup a local cache of most recently accessed data (cache, because mobiles have strict storage limits, server and db not so). So, any change from user side is also committed to this cache along side propagating to the server. All this better be encapsulated by specialized data classes, rather than relying on a back-stack or special code in activity classes.

As a pattern you can construct Model classes for data entities involved. Write a web API interface implementation to talk to the server. And then, place a cache layer before the API interface. The cache would retain outgoing changes and incoming updates to/from API layer and simply reflect data requests when server call is not needed.

Cache has to do 3 things mainly:

  1. Evict: as new data comes, drop the least important data, so cache remains of a fixed size. Most cache implementations (like this one) do this automatically.
  2. Invalidate: Some times, due to a user action, or external event on server side some data has to be refreshed.
  3. Expire: Data can be set with a time limit and will be auto evicted. This is helpful when enforcing a periodic refresh of data.

Now most cache implementations out there deal in raw bytes. I'd recommend using something like Realm, an object db and wrap it in cache like features. Hence a typical flow of requesting user tweets would be:

  1. Activity is displayed.
  2. Activity binds to data service, and expresses its interest in "tweets".
  3. Data service looks in Tweets table of cache db for last fetched list of tweets and immediately returns that.
  4. But, Data service also calls the server to give the tweets after time-stamp of latest tweet it has locally in db.
  5. Server returns latest set of tweets.
  6. Data service updates all the bound activities who expressed interest in tweets, with new incoming data. This data is also replicated locally in cache db. At this point, tweets table is also optimized by dropping old records.
  7. Activity unbinds from Data service, as the activity is going away, stopped, destroyed etc.
  8. If no bound activity is interested in "tweets", Data service stops loading more tweets.

Your data implementation can go a level further by maintaining socket connections to server and receive interesting changes in real-time. That is step 5 above will be called by server whenever there is new data incoming.

TLDR; Separate data management from Activities, and then you can evolve it independently of UI concerns.

S.D.
  • 29,290
  • 3
  • 79
  • 130
  • Great answer, thanks! What about removing expired data from the cache in order to free storage space? For instance user went back to `A` (root of backstack), it means that cached data is not required anymore, how can I know that? – user1049280 Oct 22 '15 at 10:30
  • @user1049280 updated in answer. Activities "bind" and "unbind" from a Service. – S.D. Oct 24 '15 at 06:46
-1

If you have to make changes in all activities with respect to some data you can follow the interface pattern .For example you have an custom class class ActivityData which have the content you need to update in all activities

Step 1:

Create an interface as follows

public interface ActivityEventListener
{


    ActivityData getData( Context context,
            Activity activity );


}

Step2:

Create a BaseActivity for referencing all for u r activities which u have in you application and implement the interface as follows

public class BaseActivity extends Activity
{
    protected AcitivityData mData= null;


    @Override
    protected void onCreate( Bundle savedInstanceState )
    {
        super.onCreate( savedInstanceState );
    }

    protected ActivityEventListener mListener = new ActivityEventListener()
    {


        @Override
        public ActivityData getData( Context context,
                Activity activity )
        {
            // TODO Auto-generated method stub
            return mData;
        }


    };
}

step 3: Extends your own activity with BaseActivity for example A or B or C .......

public class A extends BaseActivity {

  ActivityData mData;
@Override
protected void onCreate( Bundle savedInstanceState )
{

     mData = mListener.getData(this,this);

     updateWidgets(mData);

}

}

where updateWidgets is a function where you can define UI elements and use the data what u have with interface

As all of your activities B/C/ so on can get the reference of ActivityData. Activities form backstack will start execution through onStart() user can handle activities in same nature with the details exist in ActivityData

In your case when you like in last activity you can update the object of ActivtyData and when back stack activity resumed or started you can get updated data as your backstack activity extends with BaseActiivty having interface.

  • basically it's the same approach as @bwt suggesed how to avoid memory leaks in ActivityData? there will be some data that is not required anymore – user1049280 Oct 20 '15 at 15:53
  • yeah you are right,but this is in case of whole data in activities,for avoiding memory leaks instead of using interface user can start an activity for result with result code and can override the method onActivityResult in every activity – venkatesh venkey Oct 21 '15 at 03:45
  • that's a good point, however it will require a lot of boilerplate code to dispatch these data across backstack, also it won't work if one day I decide to switch from activities to fragments – user1049280 Oct 21 '15 at 08:02