取得活動結果

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

雖然所有 API 層級的 Activity 類別皆提供基礎 startActivityForResult()onActivityResult() API,但我們還是強烈建議您使用 AndroidX ActivityFragment 類別中導入的 Activity Result API。

Activity Result API 提供的元件可在系統分派結果後,用於註冊、啟動及處理結果。

註冊回呼以獲取活動結果

當您為了取得結果而啟動活動時,可能會因記憶體不足導致程序和活動遭到刪除;對於使用相機這類占用大量記憶體的作業而言,則幾乎可說是一定會發生這種情況。

因此,Activity Result API 會將結果回呼從程式碼中啟動另一項活動的位置分離開來。由於在重新建立程序和活動時需要用到結果回呼,因此每次活動建立時都必須無條件註冊回呼,即使啟動另一項活動的邏輯僅以使用者輸入內容或其他商業邏輯為基礎也一樣。

位於 ComponentActivityFragment 中時,Activity Result API 會提供 registerForActivityResult() API 以便註冊結果回呼。registerForActivityResult() 會取用 ActivityResultContractActivityResultCallback,然後傳回 ActivityResultLauncher,供您用於啟動另一項活動。

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

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

Kotlin

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

Java

// GetContent creates an ActivityResultLauncher<String> to let you pass
// in the mime type you want to let the user 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 want to let the user 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 Bundle savedInstanceState) {
    // ...

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

    selectButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            // Pass in the mime type you want to let the user 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() 方法。您的測試實作成果可直接呼叫 dispatchResult(),而非 startActivityForResult(),從而提供要在測試中使用的實際結果:

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

完整的測試會產生預期結果、建立測試 ActivityResultRegistry、將其傳遞至片段、直接觸發啟動器,或使用 Espresso 等其他測試 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())、還能啟動另一項活動,甚至使用 parseResult() 建構結果,就表示合約可以選擇實作 getSynchronousResult()

下列範例說明如何建構 ActivityResultContract

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