Returning Data to Previous Screens in Android Applications

When you navigate within your Android application, you usually just switch to the next screen. In some cases, when you need to pass data to that screen, the navigation involves specifying additional parameters. As I explained in the previous post about navigation in Android apps, the “next” screen doesn’t need to “know” or “assume” anything about the “previous” screen to support this scenario.

However, in some cases, you need to navigate back from the “next” screen to the “previous” one and return some kind of data to it. For example, you might have a dedicated screen in your application for choosing an image. This screen will be navigated to from several other screens, and when the user chooses an image, the previous screen should be brought from the backstack and notified of user’s choice.

In this post, I’m going to describe several approaches that you can use in your Android application to return data to previous screens.

Start Activity for Result

The oldest and the most widely used approach to pass data to the previous screen is to call startActivityForResult() method:

public static void startForResult(Activity activity, int requestCode) {
    Intent intent = new Intent(activity, SecondActivity.class);
    activity.startActivityForResult(intent, requestCode);
}

Then, in the next Activity, when you want to return data to the previous screen, you do something along these lines:

private void returnResult(Result result) {
    Intent returnIntent = new Intent();
    returnIntent.putExtra("result", (Serializable) result);
    setResult(Activity.RESULT_OK, returnIntent);
    finish();
}

After SecondActivity finishes, FirstActivity will be popped from the backstack and its onActivityResult() method will be called:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    if (requestCode == SECOND_ACTIVITY_REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            // handle data
        } else {
            // handle cancellation
        }
    } else {
        super.onActivityResult(requestCode, resultCode, data);
    }
}

The above approach works when executed from Activities, but requires a bit more care if you implement your screens using Fragments. To get onActivityResult() callback inside Fragment, make sure to follow the following guidelines:

  1. Call startActivityForResult() and not requireActivity().startActivityForResult() from within Fragment.
  2. Since the enclosing Activity always gets the first shot at handling the results, if you override onActivityResult() in the host Activity, make sure to call through to super.onActivityResult() for all non-handled cases. Handling of these cases will be delegated to your Fragments.
  3. If you want to get onActivityResult() calls within nested Fragments (e.g. ViewPager), read this answer on StackOverflow.

So, that’s the most popular way to pass data back to the previous screen, but I rarely use it myself.

See, using Intent for data transmission introduces quite a bit of semantic coupling between screens because they’ll need to be in sync with respect to the keys and the types of data used in that Intent. This kind of coupling is obscure and difficult to understand. In addition, if you’ll want to have unit-testable controllers for your screens, you’ll want to unit test the logic that both puts the data into that Intent and retrieves it on another screen. However, unit testing Intents is tricky, and I surely don’t want to introduce Robolectric just to make it simpler.

All these issues stem from the fact that startActivityForResult() mechanism is intended to be used for IPC communication. In other words, the started Activity doesn’t necessarily belong to the same process, so this mechanism should be very generic. However, when you start Activities within your own app, they’ll run in the same process. Isn’t there a simpler and safer solution for these situations? Yes, there is!

However, before I describe this solution, let’s discuss Fragments-based navigation. See, starting Activities for result works only if the next screen is a standalone Activity. What if the next screen is just another Fragment?

Set Target Fragment

If the next screen is implemented using Fragment, startActivityForResult() won’t help. In this case, you’ll need to use setTargetFragment() API when you navigate to the next Fragment:

public void toSecondScreen(int requestCode) {
    Fragment fragment = SecondFragment.newInstance();
    fragment.setTargetFragment(mFragNavController.getCurrentFrag(), requestCode);
    mFragNavController.pushFragment(fragment);
}

FragNavController object used for the above navigation is part of FragNav library described in my previous post.

Then, inside SecondFragment, you can get a reference to the previous Fragment in this manner:

Fragment targetFragment = getTargetFragment();

At this point, you can either manually call onActivityResult() on the previous Fragment:

private void returnResult(Result result) {
    Intent returnIntent = new Intent();
    returnIntent.putExtra("result", (Serializable) result);
    int requestCode = getTargetRequestCode();
    Fragment targetFragment = getTargetFragment();
    targetFragment.onActivityResult(requestCode, Activity.RESULT_OK, returnIntent);
    mScreensNavigator.navigateBack();
}

or cast it to a more specific type and call a custom method:

private void returnResult(Result result) {
    Fragment targetFragment = getTargetFragment();
    ((FirstFragment) targetFragment).setReturnData(result);
    mScreensNavigator.navigateBack();
}

Most examples of using target Fragments you’ll find on the internet will just call onActivityResult(), but I don’t think it’s a good idea.

See, during the standard Fragment’s lifecycle, onActivityResult() will be called after onStart(). This means that the Fragment receives this callback when it’s in “active” state. However, if you just directly call this method from another Fragment, chances are that the previous Fragment will be backstacked and stopped. Therefore, you’ll be invoking onActivityResult() “spuriously” in respect to standard Fragment’s lifecycle. You can make this work, but it’s risky.

For example, if you’d like to perform additional navigation in response to this event, then your application will crash with the notorious IllegalStateException, unless you use “allow state loss” flag. However, even if you do use that flag, you’ll be invoking navigation to the next screen before you returned from the “current next” screen. Again, you can make this work, but these are the kind of hacks that lead to very subtle and difficult bugs in the future.

On the other hand, if you cast the target Fragment to a more specific type, you immediately introduce strong coupling to the previous screen. Then, if you’d like to navigate to that SecondFragment from additional screens in the future, that wouldn’t be possible.

One way to mitigate this problem is to use callback interface that previous Fragments will need to implement:

private void returnResult(Result result) {
    Fragment targetFragment = getTargetFragment();
    ((SecondFragmentCallback) targetFragment).setReturnData(result);
    mScreensNavigator.navigateBack();
}

This approach is cleaner, but it still falls short of my definition of clean design. First of all, it perpetuates inheritance abuse which is so common in Android framework. Second, there is still no way to pass data back directly to a standalone unit testable controller (if such a controller is implemented on the previous screen). Third, if I’d want to refactor one of the previous screens to be Activity, this approach would require modifications in many places.

All in all, passing data back between Fragments using the “standard” approach isn’t that simple either, and is actually quite dirty.

Intra-Process Communication

Until now, I described two main approaches that you can use to pass data to previous screens from either Activities or Fragments. Both of them work, but they’re too complicated for the task at hand and introduce some unhealthy coupling into your application.

In my opinion, the situation around startActivityForResult() and setTargetFragment() is similar to the case of broadcasts in Android. If you need to send a system-wide broadcast outside of your app’s process, BroadcastManager is perfectly alright. However, the same API for intra-process communication, LocalBroadcastManager, was a total disaster. At some point, even Google realized that and Jetpack’s page about LocalBroadcastManager basically says: “don’t use this class”.

Inside your application you usually want to use simple method calls that take your own data structures as parameters, as opposed to Android’s Intents, Bundles, etc. Can we achieve something like that for this use case of passing data back to previous screens? Yes, we can.

Screens Data Return Buffer

In my applications I pass data back to previous screens using a dedicated global object. Global here means that one instance of this object will be shared among all clients. Like Singleton, but without static calls. Let me show you how this approach works, combined with ScreenNavigator abstraction described in the previous post.

Inside the original Fragment, Activity or standalone controller I’ll call one of ScreenNavigator’s method, passing “async completion token” string as a parameter:

mScreensNavigator.toSecondScreen(SECOND_SCREEN_ASYNC_COMPLETION_TOKEN);

Async completion token is a general name for a widespread design pattern. When you use this pattern, you simply designate async call with an ID and then pass that ID alongside other information to an async API. This ID isn’t used during the call, but later returned to the caller when the async operation completes. Then the caller can distinguish between different async notifications of the same type. For example, request code parameter passed into startActivityForResult() is also an async completion token.

Then, on the next screen, once the user chooses an image (for example), I’ll put the information about user’s choice into my global object and navigate back:

public void onImageClicked(Image image) {
    mScreenDataReturnBuffer.putImage(image, mAsyncCompletionToken);
    mScreensNavigator.navigateBack();
}

public void onCancelClicked() {
    mScreenDataReturnBuffer.putImage(null, mAsyncCompletionToken);
    mScreensNavigator.navigateBack();
}

After the previous screen will be brought from the backstack, in its onStart() method (or an alternative method, like Conductor’s onAttach()), I’ll do the following:

@Override
public void onStart() {
    super.onStart();

    if (mScreenDataReturnBuffer.hasDataForToken(SECOND_SCREEN_ASYNC_COMPLETION_TOKEN)) {
        Image image = mScreenDataReturnBuffer.getImage(SECOND_SCREEN_ASYNC_COMPLETION_TOKEN);
        if (image != null) {
            // handle user's choice
        } else {
            // handle operation cancelled
        }
    }
}

That’s all. I’ve just passed the data back to the previous screen in simple, explicit and type-safe manner. The only coupling that I introduced is the assumption, made by the previous screen, that the next screen will put a specific type of data into that global buffer.

Have you noticed the big architectural benefit of this approach? It’s completely independent of implementation details of both the previous and the next screens. You can use either Activities, or Fragments, or whatever, and this approach will work exactly the same in all situations. In addition, it’ll be very easy to unit test the controllers that will use this buffer if you’ll want to do that. Lastly, since you control the implementation of this buffer, you can implement advanced debugging and logging strategies inside it. For example, you can fail-fast and throw an exception if developers make mistakes in the future and try to get data from the buffer using non-existent async completion token.

All in all, this is a very simple technique which, nonetheless, brings some major benefits into your codebase.

Process Death Considerations

Now I’m forced to complicate the simple picture I drew in the previous section with a bit of a nuance.

Since the aforementioned buffer is global, it’ll be preserved on configuration changes. However, since it keeps the data in-memory, it’ll be cleared on process death. Therefore, if process death will occur after the next screen pushes data into the buffer and before the previous screen retrieves it, that data will be lost.

To avoid losing data due to process death, you need to make sure that you put it into the buffer right before a call to navigate back, and retrieve it from the buffer in the subsequent onStart() callback of the previous screen. This worked for me flawlessly so far. However, if you want to be on the safe side, you might want to either implement persistent caching, or make sure that you preserve buffer’s contents across process death by delegating onSaveInstanceState() and onRestoreInstanceState()to it.

In my experience it wasn’t a major complication, but it is something that you should keep in mind.

Summary

Following my two previous articles, several developers asked me about how I pass data back to previous screens, so I decided to do a short post on that. However, as is often the case in Android, something “simple” that we’re accustomed to, can be quite messy to unpack. With all the nuance and details, this post ended up being another deep-dive.

I showed you how I address the cases when I need to pass data back to previous screens in my applications. When I need to reach out to external apps (e.g. camera app), I just use the standard startActivityForResult() and don’t even try to make it part of my ScreenNavigator. After all, these scenarios are relatively rare, so I’m alright with treating them as exceptions.

Edit: several readers asked about using Activity-scoped ViewModel to exchange data between Fragments. I shared my opinion about that in this Twitter thread.

Thank you for reading and don’t forget to subscribe to my newsletter if you liked this article.

Check out my premium

Android Development Courses

12 comments on "Returning Data to Previous Screens in Android Applications"

    • Pablo, thanks for the link.
      While it’s indeed yet another alternative, it only reinforces my negative opinion about Navigtaion Component that I shared in the previous posts. I wouldn’t want to have this kind of code (from this link) in my projects under any circumstances:

      findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData("key")?.observe(viewLifecycleOwner) { ... }

      Regards

      Reply
      • I also consider the Navigation API is not the best. However, even though many developers don’t like it, there are many companies using it just for the fact of “This is what Google recommends”. I dislike that code either but you know, those using the Jetpack Navigation will have to live with it.

        Reply
        • I see your point now. Yes, that’s, unfortunately, very true. I’m sure I’ll need to deal with Navigation Component sooner or later too, but hope to postpone this moment.
          Thanks for the link

          Reply
    • Hello Piotr,
      I haven’t worked with Navigation Component so far, and I don’t intend to work with it in the future, unless forced. However, Pablo in one of the previous comments shared a link to an issue where Google engineers described how you’d do that with Navigation Component. I guess that’s what you’re looking for.

      Reply
    • Nice summery,

      from my experience, having a global in memory repo can also drawbacks.

      Going forward with your example: besides choosing an image from the gallery, you might also need to take a picture, handle permiassions or you are in a wizard with many screens and you can go back & forth between them.

      1. In this case your onStart() method will end up with a huge if-else and nobody will understand your code except you.

      2. You might also need to reuse your Return Buffer, e.g.: starting the same screen from different places. If somewhere a developer forget adding mScreenDataReturnBuffer.putImage(null, mAsyncCompletionToken);
      you will end up with regression issues.

      3. Similar to point 2: since the DataReturnBuffer is not immutable and anyone has access to it, there is no single source of truth.

      Overall, using an in memory repo for data returning to previous screen is not safe, scalable and its hard to maintain.

      Reply
      • Hello Gyorgy,
        I guess you commented on the post itself, but unintentionally replied tp Piotr’s comment, so I’ll jump in.

        1a. I’m not sure what “huge if-else” you’re referring to. From the perspective of the “previous” screen it doesn’t matter how the image is obtained. Whether the next screen will implement gallery browser, or custom camera, or whatever – the end result is just one single image in the buffer (or a list of images, in case of multi-select). In addition, if screens become complex, I always implement per-screen FSMs. Therefore, onStart() will contain “switch” on the current state, so any decent developer will immediately understand the logic.

        1b. Do you have any alternative solution that will be simpler and more understandable long-term?

        2. Again, not sure I understand the scenario. However, in general, any kind of convention can be violated. These are just bugs. If you know a method to write software without bugs, please share it with me.

        3. If you have one global buffer that all screens use to pass data back to previous screens, how isn’t it “a single source of truth”? What does mutability have to do with it? What kind of problem do you foresee?

        Overall, my experience with this approach suggests that it’s safe and easy to maintain. As for “not scalable”, I’m once again not sure what you mean by this generic claim.

        Reply
    • Hi,
      I don’t have any open-sourced example with ScreenDataReturnBuffer. That said, I don’t see why it would need anything special to accomodate ScreenNavigator.navigateUp() calls. If you navigate back/up, then you either push some kind of “action cancelled” state into the buffer, or you don’t push anything and then the previous screen “knows” that it means cancellation.

      Reply
  1. Hello,

    I was having hard time deciding how to implement ScreenDataReturnBuffer.
    At first, I thought it could be implemented with Map. But then I was confused what to do when there are multiple types of data being forwarded between screens ?

    So here I tried to implement it with Map. Will you please verify if I am going in the right direction or not ?
    ——————
    public class ScreenDataReturnBuffer {

    private Map mStringBuffer = new HashMap();

    private void putImageUrl(String imageUrl, String asyncCompletionToken) {
    mStringBuffer.put(asyncCompletionToken, imageUrl);
    }

    public boolean hasDataForToken(String asyncCompletionToken) {
    return mStringBuffer.containsKey(asyncCompletionToken);
    }

    public String getImageUrl(String asyncCompletionToken) {
    return mStringBuffer.remove(asyncCompletionToken);
    }

    }
    ——————
    So if this is right, then should we use multiple Map for different data types ??

    Reply
    • Hello Dhaval,
      Sounds like you’re trying to “generalize” the data structure inside ScreenDataReturnBuffer.
      If you expect that you’ll need to hold multiple image URLs in this cache, then having a Map is alright, but I never actually needed that myself. I just store a single URI and a single AsyncCompletionToken there. Furthermore, the name mStringBuffer suggests that you want to “generalize” this data structure to all strings, which feels like a non-optimal approach.
      I think you can have multiple Maps there, but not for “different data types”, but for “different use cases”. Therefore, if you need to pass back strings in two different scenarios and these stings represent different info, use separate data structures for them.

      Reply

Leave a Comment