從活動取得結果

無論是在您的應用程式內還是從另一個應用程式開始另一個活動,都不一定只能是單向作業。您也可以開始另一個活動並取得結果。舉例來說,您的應用程式可以啟動相機應用程式,並取得拍攝的相片做為結果。或者您也可以啟動「聯絡人」應用程式,讓使用者可以選取聯絡人,藉此取得聯絡人詳細資料做為結果。

雖然基礎 startActivityForResult()onActivityResult() API 都可在所有 API 層級於 Activity 類別使用,但仍非常建議您使用 AndroidX ActivityFragment 中導入的活動結果。

Activity Result API 提供元件,以在系統分派結果時註冊、啟動和處理結果。

註冊活動結果的回呼

開始執行活動以取得結果時,可能會因為記憶體不足導致程序和活動遭到刪除 (如果是使用相機等作業,必定造成記憶體不足)。

因此,Activity Result API 會從啟動其他活動的程式碼所在位置分離出結果回呼。因此,回呼必須在重建程序和活動時可用,回呼必須在每次建立活動時無條件註冊,即使啟動其他活動的邏輯僅根據使用者輸入內容或其他商業邏輯而發生。

ComponentActivityFragment 中,Activity Result API 提供 registerForActivityResult() API 以註冊結果回呼。registerForActivityResult() 會取用 ActivityResultContractActivityResultCallback,然後回傳 ActivityResultLauncher,然後您將以此啟動其他活動。

ActivityResultContract 會定義所需的輸入內容類型,以產生結果與結果的輸出內容類型。API 會針對拍照、要求取得權限等基本意圖動作提供預設合約。您也可以建立自己的自訂合約

ActivityResultCallback 是一種單一方法介面,具有取用 ActivityResultContract 中定義的輸出內容類型物件的 onActivityResult() 方法:

Kotlin

val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
    // Handle the returned Uri
}

Java

// GetContent creates an ActivityResultLauncher<String> to allow you to pass
// in the mime type you'd like to allow the user to select
ActivityResultLauncher<String> mGetContent = registerForActivityResult(new GetContent(),
    new ActivityResultCallback<Uri>() {
        @Override
        public void onActivityResult(Uri uri) {
            // Handle the returned Uri
        }
});

如果有多個活動結果呼叫,且這些呼叫使用不同的合約或需要個別回呼,您就可以多次呼叫 registerForActivityResult() 以註冊多個 ActivityResultLauncher 執行個體。建立片段或活動時,都必須以相同的順序呼叫 registerForActivityResult(),以確保活動結果都會傳送到正確的回呼。

建立片段或活動之前呼叫 registerForActivityResult() 較為妥當,因為這在傳回的 ActivityResultLauncher 執行個體中宣告成員變數時,就可以直接使用它。

啟動活動以取得結果

儘管 registerForActivityResult() 會登錄回呼,但不會啟動其他活動並發起要求來取得結果。相反的,這個動作是由傳回的 ActivityResultLauncher 執行個體負責。

只有符合 ActivityResultContract 類型的既有輸入內容,啟動器才會接受。呼叫 launch(),才會開始產生結果的程序。使用者完成後續活動並回傳時,系統才就會從 ActivityResultCallback 執行 onActivityResult(),如以下範例所示:

Kotlin

val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
    // Handle the returned Uri
}

override fun onCreate(savedInstanceState: Bundle?) {
    // ...

    val selectButton = findViewById<Button>(R.id.select_button)

    selectButton.setOnClickListener {
        // Pass in the mime type you'd like to allow the user to select
        // as the input
        getContent.launch("image/*")
    }
}

Java

ActivityResultLauncher<String> mGetContent = registerForActivityResult(new GetContent(),
    new ActivityResultCallback<Uri>() {
        @Override
        public void onActivityResult(Uri uri) {
            // Handle the returned Uri
        }
});

@Override
public void onCreate(@Nullable savedInstanceState: Bundle) {
    // ...

    Button selectButton = findViewById(R.id.select_button);

    selectButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            // Pass in the mime type you'd like to allow the user to select
            // as the input
            mGetContent.launch("image/*");
        }
    });
}

超載的 launch() 版本可讓您傳遞輸入內容以及 ActivityOptionsCompat

以個別類別接收活動結果

雖然 ComponentActivityFragment 類別會實作 ActivityResultCaller 介面以允許使用 registerForActivityResult() API,但直接使用 ActivityResultRegistry,也會以個別類別接收活動結果,且不會實作 ActivityResultCaller

舉例來說,您也許想要可以實作可處理登錄合約的 LifecycleObserver,然後啟動啟動器:

Kotlin

class MyLifecycleObserver(private val registry : ActivityResultRegistry)
        : DefaultLifecycleObserver {
    lateinit var getContent : ActivityResultLauncher<String>

    override fun onCreate(owner: LifecycleOwner) {
        getContent = registry.register("key", owner, GetContent()) { uri ->
            // Handle the returned Uri
        }
    }

    fun selectImage() {
        getContent.launch("image/*")
    }
}

class MyFragment : Fragment() {
    lateinit var observer : MyLifecycleObserver

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        observer = MyLifecycleObserver(requireActivity().activityResultRegistry)
        lifecycle.addObserver(observer)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val selectButton = view.findViewById<Button>(R.id.select_button)

        selectButton.setOnClickListener {
            // Open the activity to select an image
            observer.selectImage()
        }
    }
}

Java

class MyLifecycleObserver implements DefaultLifecycleObserver {
    private final ActivityResultRegistry mRegistry;
    private ActivityResultLauncher<String> mGetContent;

    MyLifecycleObserver(@NonNull ActivityResultRegistry registry) {
        mRegistry = registry;
    }

    public void onCreate(@NonNull LifecycleOwner owner) {
        // ...

        mGetContent = mRegistry.register(“key”, owner, new GetContent(),
            new ActivityResultCallback<Uri>() {
                @Override
                public void onActivityResult(Uri uri) {
                    // Handle the returned Uri
                }
            });
    }

    public void selectImage() {
        // Open the activity to select an image
        mGetContent.launch("image/*");
    }
}

class MyFragment extends Fragment {
    private MyLifecycleObserver mObserver;

    @Override
    void onCreate(Bundle savedInstanceState) {
        // ...

        mObserver = new MyLifecycleObserver(requireActivity().getActivityResultRegistry());
        getLifecycle().addObserver(mObserver);
    }

    @Override
    void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button selectButton = findViewById(R.id.select_button);
        selectButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mObserver.selectImage();
            }
        });
    }
}

使用 ActivityResultRegistry API 時,強烈建議您使用可取用 LifecycleOwner 的 API,因為 LifecycleOwner 會在 Lifecycle 刪除時自動移除您登錄的啟動器。然而,如果遇到 LifecycleOwner 無法使用的情況,ActivityResultLauncher 類別可讓您改用手動呼叫 unregister()

測試

根據預設,registerForActivityResult() 會自動使用活動提供的 ActivityResultRegistry。這也提供超載,讓您不需啟動其他活動,只需傳送自己的 ActivityResultRegistry 執行個體就能測試活動結果呼叫。

測試應用程式的片段時,使用 FragmentFactoryActivityResultRegistry 傳入片段的建構函式,即可得到測試用的 ActivityResultRegistry

舉例來說,使用 TakePicturePreview 合約取得圖片縮圖的片段時,可使用類似以下方式撰寫:

Kotlin

class MyFragment(
    private val registry: ActivityResultRegistry
) : Fragment() {
    val thumbnailLiveData = MutableLiveData<Bitmap?>

    val takePicture = registerForActivityResult(TakePicturePreview(), registry) {
        bitmap: Bitmap? -> thumbnailLiveData.setValue(bitmap)
    }

    // ...
}

Java

public class MyFragment extends Fragment {
    private final ActivityResultRegistry mRegistry;
    private final MutableLiveData<Bitmap> mThumbnailLiveData = new MutableLiveData();
    private final ActivityResultLauncher<Void> mTakePicture =
        registerForActivityResult(new TakePicturePreview(), mRegistry, new ActivityResultCallback<Bitmap>() {
            @Override
            public void onActivityResult(Bitmap thumbnail) {
                mThumbnailLiveData.setValue(thumbnail);
            }
        });

    public MyFragment(@NonNull ActivityResultRegistry registry) {
        super();
        mRegistry = registry;
    }

    @VisibleForTesting
    @NonNull
    ActivityResultLauncher<Void> getTakePicture() {
        return mTakePicture;
    }

    @VisibleForTesting
    @NonNull
    LiveData<Bitmap> getThumbnailLiveData() {
        return mThumbnailLiveData;
    }

    // ...
}

建立測試專用的 ActivityResultRegistry 時,必須實作 onLaunch() 方法。與其呼叫 startActivityForResult() 進行實作,直接呼叫 dispatchResult(),更能在測試中得到想要的實際結果:

val testRegistry = object : ActivityResultRegistry() {
    override fun <I, O> onLaunch(
            requestCode: Int,
            contract: ActivityResultContract<I, O>,
            input: I,
            options: ActivityOptionsCompat?
    ) {
        dispatchResult(requestCode, expectedResult)
    }
}

完整的測試會產生預期結果。建立測試 ActivityResultRegistry,將其傳送至片段,觸發啟動器 (可以直接或透過 Expresso 等其他測試 API),然後驗證結果:

@Test
fun activityResultTest {
    // Create an expected result Bitmap
    val expectedResult = Bitmap.createBitmap(1, 1, Bitmap.Config.RGBA_F16)

    // Create the test ActivityResultRegistry
    val testRegistry = object : ActivityResultRegistry() {
            override fun <I, O> onLaunch(
            requestCode: Int,
            contract: ActivityResultContract<I, O>,
            input: I,
            options: ActivityOptionsCompat?
        ) {
            dispatchResult(requestCode, expectedResult)
        }
    }

    // Use the launchFragmentInContainer method that takes a
    // lambda to construct the Fragment with the testRegistry
    with(launchFragmentInContainer { MyFragment(testRegistry) }) {
            onFragment { fragment ->
                // Trigger the ActivityResultLauncher
                fragment.takePicture()
                // Verify the result is set
                assertThat(fragment.thumbnailLiveData.value)
                        .isSameInstanceAs(expectedResult)
            }
    }
}

建立自訂合約

雖然 ActivityResultContracts 包含許多預先建立的 ActivityResultContract 類別可供使用,但您可以提供自訂合約,提供確實符合需求的 API 類別。

每個 ActivityResultContract 都需要定義輸入內容與輸出內容類別,如果不需輸入任何輸入內容,則使用 Void 做為輸入內容類型 (在 Kotlin 中則使用 Void?Unit)。

每個合約須實作 createIntent() 方法,而這需要 Context 及輸入內容,並建構會搭配 startActivityForResult() 使用的 Intent

每個合約也須實作 parseResult(),以使用特定的 resultCode 產生輸出內容 (例如:Activity.RESULT_OKActivity.RESULT_CANCELED) 和 Intent

如果可以針對特定輸入內容判別結果,並且不需呼叫 createIntent(),就可以選擇實作 getSynchronousResult() 以啟動其他活動,並使用 parseResult() 建構結果。

Kotlin

class PickRingtone : ActivityResultContract<Int, Uri?>() {
    override fun createIntent(context: Context, ringtoneType: Int) =
        Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
            putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType)
        }

    override fun parseResult(resultCode: Int, result: Intent?) : Uri? {
        if (resultCode != Activity.RESULT_OK) {
            return null
        }
        return result?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
    }
}

Java

public class PickRingtone extends ActivityResultContract<Integer, Uri> {
    @NonNull
    @Override
    public Intent createIntent(@NonNull Context context, @NonNull Integer ringtoneType) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType.intValue());
        return intent;
    }

    @Override
    public Uri parseResult(int resultCode, @Nullable Intent result) {
        if (resultCode != Activity.RESULT_OK || result == null) {
            return null;
        }
        return result.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
    }
}

如果不需要自訂合約,就可以使用 StartActivityForResult 合約。此一般合約會取用任何 Intent 做為輸入內容,並回傳 ActivityResult,讓您可以擷取 resultCodeIntent 做為回呼的一部分,如以下範例所示:

Kotlin

val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
    if (result.resultCode == Activity.RESULT_OK) {
        val intent = result.data
        // Handle the Intent
    }
}

override fun onCreate(savedInstanceState: Bundle) {
    // ...

    val startButton = findViewById(R.id.start_button)

    startButton.setOnClickListener {
        // Use the Kotlin extension in activity-ktx
        // passing it the Intent you want to start
        startForResult.launch(Intent(this, ResultProducingActivity::class.java))
    }
}

Java

ActivityResultLauncher<Intent> mStartForResult = registerForActivityResult(new StartActivityForResult(),
        new ActivityResultCallback<ActivityResult>() {
    @Override
    public void onActivityResult(ActivityResult result) {
        if (result.getResultCode() == Activity.RESULT_OK) {
            Intent intent = result.getData();
            // Handle the Intent
        }
    }
});

@Override
public void onCreate(@Nullable savedInstanceState: Bundle) {
    // ...

    Button startButton = findViewById(R.id.start_button);

    startButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            // The launcher with the Intent you want to start
            mStartForResult.launch(new Intent(this, ResultProducingActivity.class));
        }
    });
}