Brane
Brane

Reputation: 810

Alternatives to startActivityForResult

Edit: changed Fragment to Partial, I was ignorant of the Fragment object when I wrote this.

I have a partial that contains a button to bring up the contact list. Doing this requires calling

startActivityForResult( new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI), MY_REQUEST_CODE );

and handling the result in my Activity, something like:

public void onActivityResult( int requestCode, int resultCode, Intent data ) {
    if (resultCode == RESULT_OK) {
        switch (requestCode) {
            case MY_REQUEST_CODE: {
                Address address = contact_address( data );
                if (address != null) {
                    // do something with address
                }
            } break;
        }
    }
}

Depending on how I include that partial in my Activity layout, it may be several layers deep in other partials, and there may be more than one instance of the partial.

I would like to avoid propagating the ID of MY_REQUEST_CODE all the way down to the partial that invokes the activity - or any variation thereof, like assigning an onClickListener to the button - I don't want the top level UI to care about how the partial is constructed at all.

Is there a standard way of achieving this? It seems to me that if onActivityResult could be made to accept Uri's instead of int codes, the propagation could have been avoided. I hope I am missing something obvious here...

Upvotes: 6

Views: 14115

Answers (2)

Brane
Brane

Reputation: 810

Neither of the two methods here works. Both are prone to a problem when the calling Activity is replaced by the system (for instance, via screen rotation).

In Commonsware's answer, the OnContactPickedListener assigned by requestContact() will refer to a control that no longer exists.

In my own answer, the listener retained by the Activity will vanish, and the new activity's equivalent listener will be null.

However there is a solution, which relies on making the context of the listener reassignable.

Drawing on my previous solution, recast the ActivityResultListener interface as a static class:

abstract public static class ActivityResultListener {
    private Context
        context_;
    public ActivityResultListener( Context context ) {
        context_ = context;
    }
    public Context getContext() { return context_; }
    public void setContext( Context context ) { context_ = context; }

    abstract public void onResultCode( int resultCode, Intent data );
}

Set up an internal class to record state for the BaseActivity:

protected static class BaseState {
    private final ActivityResultListener
        activity_result_listener_;

    protected BaseState( BaseActivity activity ) {
        activity_result_listener_ = activity.activity_result_listener_;
    }

    protected void setState( BaseActivity activity ) {
        activity.activity_result_listener_ = activity_result_listener_;
        if (activity.activity_result_listener_ != null) {
            activity.activity_result_listener_.setContext( activity );
        }
    }
}

Note especially the call to setContext() in setState(). This avoids the problems associated with non-static interface implementations, i.e. that their references vanish when the Activity is recreated.

Retain state from within the BaseActivity:

@Override
public Object onRetainNonConfigurationInstance() {
    return new BaseState( this );
}

Restore state from within BaseActivity.onCreate()

Object state = getLastNonConfigurationInstance();
if (state instanceof BaseState) {
    ((BaseState)state).setState( this );
}

In the implementation of ActivityResultListener, be sure to use getContext() and findViewById() to dereference everything on demand rather than storing references:

private static class ContactChoiceListener extends BaseActivity.ActivityResultListener {
    private final int 
        id_;

    public ContactChoiceListener( Context context, int id ) {
        super( context );
        id_ = id;
    }

    @Override
    public void onResultCode( int resultCode, Intent data ) {
        if (resultCode == BaseActivity.RESULT_OK) {
            AddressEditor editor = (AddressEditor)((BaseActivity)getContext()).findViewById( id_ );
            if (editor != null)
                editor.add_contact_address( data );
        }
    }
}

Whew. And the best part is, this is all obsolete because Fragments have a completely different way of dealing with state, using setRetainInstance(boolean).

I will be implementing that version shortly, will post here if there is interest.

Upvotes: 2

Brane
Brane

Reputation: 810

I didn't feel that Commonsware's solution answered my question, because it required every container of the partial to add handlers for events that are contained wholly within the partial.

I specifically do not want to have to implement handlers for every instance of that partial.

So I came up with a solution of sorts, though I admit it doesn't feel right either.

First, I subclass Activity, and create a small framework for associating a listener with startActivityForResult() and onActivityResult().

public class BaseActivity extends Activity {
    // assume that we'll never start more than one activity at a time from our activity (a safe assumption?)
    private static final int
        LISTENED_REQUEST_CODE = 1000000000;

    public static interface ActivityResultListener {
        public void onResultCode( int resultCode, Intent data );
    }
    private ActivityResultListener
        activity_result_listener_;

    public void startActivityForResult( Intent intent, ActivityResultListener listener ) {

        // paranoia
        if (activity_result_listener_ != null) {
            Log.e( TAG, "Activity trying to start more than one activity at a time..." );
            return;
        }

        activity_result_listener_ = listener;
        startActivityForResult( intent, LISTENED_REQUEST_CODE );
    }

    public void onActivityResult( int requestCode, int resultCode, Intent data ) {
        if (requestCode == LISTENED_REQUEST_CODE) {
            if (activity_result_listener_ != null) {
                ActivityResultListener listener = activity_result_listener_;
                activity_result_listener_ = null;
                listener.onResultCode( resultCode, data );
                return;
            }
        }

        super.onActivityResult(requestCode, resultCode, data);
    }
}

Then inside the partial, I call my overloaded startActivityForResult() and implement a listener:

public void onFinishInflate() {
    ImageButton contact_button = (ImageButton)findViewById(R.id.contact_button);
    contact_button.setOnClickListener( new OnClickListener() {
        @Override
        public void onClick(View view) {
            ((BaseActivity)getContext()).startActivityForResult( new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI), 
                new BaseActivity.ActivityResultListener() {
                    @Override
                    public void onResultCode( int resultCode, Intent data ) {
                        if (resultCode == BaseActivity.RESULT_OK) {
                            add_contact_address( data );
                        }
                    }
                });
        }
    } ); 
}

So now I can use this partial all over the place without having to define listeners for each instance.

The drawback I see is that subclassing Activity will prevent me from using other Activity types. This could be reworked into an interface/implementation, but then starts to suffer from non-DRY logic once more.

Upvotes: 3

Related Questions