startActivityForResult的替代方案

时间:2011-10-17 16:22:08

标签: android

编辑:将Fragment更改为Partial,当我写这篇文章时,我对Fragment对象一无所知。

我有一个部分,其中包含一个按钮以显示联系人列表。这样做需要调用

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

并在我的Activity中处理结果,例如:

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;
        }
    }
}

根据我在Activity布局中包含部分的方式,在其他部分中可能有几层深度,并且可能有多个部分实例。

我想避免将MY_REQUEST_CODE的ID一直传播到调用活动的部分 - 或其任何变体,例如将onClickListener分配给按钮 - 我不希望顶级UI关注部分是如何构建的。

是否有实现这一目标的标准方法?在我看来,如果可以使onActivityResult接受Uri而不是int代码,则可以避免传播。我希望我错过了一些明显的东西......

3 个答案:

答案 0 :(得分:12)

  

我有一个片段,其中包含一个按钮以显示联系人列表。这样做需要调用[startActivityForResult()]

最简单的答案是,您可以在startActivityForResult()而不是Fragment上致电Activity。结果应该回到片段自己的onActivityResult()

话虽如此,你的方法似乎是对我推荐的几乎完全倒置。碎片不应该是开始活动。片段既不知道也不关心如何处理片段本身之外的事物的特定UI事件(例如,按钮点击)。在这种情况下,活动应该负责获得联系,片段只是告诉活动“嘿,这里按钮gots按下,哟,给我一个联系回”。这不仅对于分离关注点很重要,而且对于测试很重要,因此您可以使用模拟的联系人选择等来测试此行为。

  

根据我在Activity布局中包含该片段的方式,它可能在其他UI片段中有几层深度

您无法在Android中的片段中嵌套片段。 If you try, you will get unreliable results

  

我不希望顶级UI关心如何构建片段。

活动绝对必须知道片段是如何构建的,因为活动就是构建片段的那个。

关闭袖口,以下是我接近它的方法:

  1. 为超出片段本身边界的片段引发的UI事件建立一个侦听器接口。出于本答案的目的,我将称之为OnFooEventListener

  2. 当活动创建/配置片段时,活动会向片段提供OnFooEventListener实例。如果活动实现了界面,那可能就是活动本身。

  3. 为片段希望某人代表其执行的异步pick-a-contact事件建立一个侦听器接口。出于本答案的目的,我将称之为OnContactPickedListener。它会有onContactPicked()这样的方法,其中包含所选联系人的Uri

  4. OnFooEventListener上,当用户点击按钮时会调用requestContact()之类的内容。 requestContact()会将OnContactPickedListener个实例作为参数。

  5. 该活动将进行startActivityForResult()调用,并在onActivityResult()中调用onContactPicked()上的关联OnContactPickedListener方法。在请求进行过程中,活动会将这些缓存在HashMap或其他内容中。

  6. 现在,我们在活动和片段之间有明确的分离。片段仍然可以由任何数量的活动托管(例如,一个用于大屏幕,一个用于普通屏幕)。可以通过生产方式(ACTION_PICK)或其他方式提供联系以进行测试(例如,作为测试用例的一部分建立的值)。该活动可以毫无问题地处理任何数量的此类碎片。

答案 1 :(得分:3)

我不觉得Commonsware的解决方案回答了我的问题,因为它要求partial的每个容器都为完全包含在partial中的事件添加处理程序。

我特别不希望为该部分的每个实例实现处理程序。

所以我提出了各种各样的解决方案,但我承认它也感觉不对。

首先,我将Activity子类化,并创建一个用于将侦听器与startActivityForResult()和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);
    }
}

然后在partial中,我调用我重载的startActivityForResult()并实现一个监听器:

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 );
                        }
                    }
                });
        }
    } ); 
}

所以现在我可以在整个地方使用这个部分而不必为每个实例定义监听器。

我看到的缺点是子类化Activity会阻止我使用其他Activity类型。这可以重新设计到接口/实现中,但之后又开始受到非DRY逻辑的影响。

答案 2 :(得分:3)

这两种方法都不适用。当调用Activity被系统替换时(例如,通过屏幕旋转),两者都容易出问题。

在Commonsware的回答中,requestContact()分配的OnContactPickedListener将引用一个不再存在的控件。

在我自己的回答中,Activity保留的监听器将消失,新活动的等效监听器将为null。

但是有一个解决方案,它依赖于重新分配监听器的上下文。

使用我之前的解决方案,将ActivityResultListener接口重新设置为静态类:

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 );
}

设置内部类以记录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 );
        }
    }
}

特别注意在setState()中调用setContext()。这避免了与非静态接口实现相关的问题,即在重新创建Activity时它们的引用会消失。

保留BaseActivity中的状态:

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

从BaseActivity.onCreate()

中恢复状态
Object state = getLastNonConfigurationInstance();
if (state instanceof BaseState) {
    ((BaseState)state).setState( this );
}

在ActivityResultListener的实现中,一定要使用getContext()和findViewById()来取消引用所有内容,而不是存储引用:

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 );
        }
    }
}

呼。最好的部分是,这已经过时,因为Fragments使用setRetainInstance(boolean)处理状态的方式完全不同。

我将很快实施该版本,如果有兴趣,将在此处发布。