取得活動結果

無論是從開發人員的應用程式,或是從他人的應用程式開始活動,都不需是單向作業。開發人員也可以啟動其他活動並取得結果。舉例來說,開發人員的應用程式可以啟動相機應用程式,並取得拍攝的相片作為結果。或者,開發人員也可以啟動「聯絡人」應用程式,讓使用者選取聯絡人,藉此取得聯絡人詳細資料作為結果。

雖然基礎 startActivityForResult()onActivityResult() API,都能在 Activity 類別使用所有 API 級別,對開發人員仍強烈建議使用 AndroidX Activity Fragment 導入的 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 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 銷毀時自動移除已登錄的啟動器。然而,要是遇上 API 不接受 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,將其傳送至片段,觸發啟動器,然後驗證結果,開發人員也可直接執行做為測試,或透過 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(),則開發人員可以選擇性實作 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));
        }
    });
}