Obtenir un résultat depuis une activité

Le lancement d'une autre activité, qu'il s'agisse d'une activité dans votre application ou dans une autre, ne doit pas nécessairement être une opération à sens unique. Vous pouvez également lancer une activité et recevoir un résultat. Par exemple, votre application peut lancer une application d'appareil photo et recevoir la photo prise comme résultat. Vous pouvez également démarrer l'application Contacts pour que l'utilisateur sélectionne un contact, puis reçoive ses coordonnées en conséquence.

Bien que les API startActivityForResult() et onActivityResult() sous-jacentes soient disponibles dans la classe Activity sur tous les niveaux d'API, Google recommande vivement d'utiliser les API de résultat d'activité introduites dans les classes AndroidX Activity et Fragment.

Les API de résultat d'activité fournissent des composants pour l'enregistrement d'un résultat, Lancer l'activité qui génère le résultat et le gérer une fois qu'il est envoyé par le système.

Enregistrer un rappel pour un résultat d'activité

Lorsque vous lancez une activité pour un résultat, il se peut (et il est même presque certain en cas d'opérations exigeantes en mémoire, comme l'utilisation de l'appareil photo) que votre processus et votre activité soient détruits en raison d'une mémoire insuffisante.

Pour cette raison, les API de résultat d'activité dissocient le rappel du résultat de l'endroit dans le code où vous lancez l'autre activité. Étant donné que le rappel du résultat doit être disponible lorsque votre processus et votre activité sont recréés, le rappel doit être enregistré sans condition chaque fois que votre activité est créée, même si la logique de lancement de l'autre activité n'intervient qu'en fonction de l'entrée utilisateur ou d'une autre logique métier.

Lorsqu'elles se trouvent dans un élément ComponentActivity ou Fragment, les API de résultat d'activité fournissent un API registerForActivityResult() pour enregistrer le rappel des résultats. registerForActivityResult() prend un élément ActivityResultContract et un élément ActivityResultCallback et renvoie un élément ActivityResultLauncher que vous utiliserez pour lancer l'autre activité.

Un élément ActivityResultContract définit le type d'entrée nécessaire pour produire un résultat avec le type de sortie du résultat. Les API fournissent des contrats par défaut pour les actions d'intent de base comme la capture de photos, les demandes d'autorisations, etc. Vous pouvez également créer un contrat personnalisé.

ActivityResultCallback est une interface à méthode unique avec une méthode onActivityResult() qui accepte un objet du type de sortie défini dans l'élément 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
        }
});

Si plusieurs appels de résultats d'activité utilisent des contrats différents ou souhaitent des rappels distincts, vous pouvez appeler registerForActivityResult() plusieurs fois pour enregistrer plusieurs instances de ActivityResultLauncher. Vous devez appeler registerForActivityResult() dans le même ordre pour chaque fragment et activité créés afin de vous assurer que les résultats en cours de transfert sont distribués au rappel approprié.

Vous pouvez appeler registerForActivityResult() avant de créer votre fragment ou votre activité afin de les utiliser directement lors de la déclaration de variables de membre pour les instances de ActivityResultLauncher renvoyées.

Lancer une activité pour obtenir un résultat

Bien que registerForActivityResult() enregistre votre rappel, il ne lance pas l'autre activité, et la requête de résultat est rejetée. Il s'agit de la responsabilité de l'instance de ActivityResultLauncher renvoyée.

Si une entrée existe, le lanceur d'applications accepte l'entrée correspondant au type de ActivityResultContract. Appeler launch() lance le processus de production du résultat. Une fois que l'utilisateur a terminé l'activité et renvoie le résultat, onActivityResult() de ActivityResultCallback est exécuté, comme l'illustre l'exemple suivant :

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/*");
        }
    });
}

Une version surchargée de launch() vous permet de transmettre un ActivityOptionsCompat en plus de l'entrée.

Recevoir un résultat d'activité dans une autre classe

Alors que les classes ComponentActivity et Fragment implémentent l'interface ActivityResultCaller pour vous permettre d'utiliser les API registerForActivityResult(), vous pouvez également recevoir le résultat de l'activité dans une classe distincte qui n'implémente pas ActivityResultCaller en utilisant directement ActivityResultRegistry.

Par exemple, vous pouvez installer un élément LifecycleObserver capable de gérer l'enregistrement d'un contrat tout en démarrant le lanceur d'applications :

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

Avec les API ActivityResultRegistry, Google vous recommande vivement d'opter pour les API qui utilisent LifecycleOwner, car LifecycleOwner supprime automatiquement le lanceur d'applications enregistré lorsque Lifecycle est détruit. Toutefois, si aucun LifecycleOwner n'est disponible, chaque classe ActivityResultLauncher vous permet d'appeler manuellement unregister() en tant qu'alternative.

Test

Par défaut, registerForActivityResult() utilise automatiquement l'élément ActivityResultRegistry fourni par l'activité. Il propose également une surcharge qui vous permet de transmettre votre propre instance de ActivityResultRegistry, qui peut être utilisée pour tester les appels de résultats d'activité sans lancer d'autres activités.

Lors du test des fragments de votre application, vous devez fournir un ActivityResultRegistry de test à l'aide d'un élément FragmentFactory pour transmettre le ActivityResultRegistry au constructeur du fragment.

Par exemple, un fragment qui utilise le contrat TakePicturePreview pour obtenir une miniature de l'image peut être écrit comme suit :

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

    // ...
}

Lorsque vous créez un élément ActivityResultRegistry pour un test, vous devez mettre en œuvre la méthode onLaunch(). Au lieu d'appeler startActivityForResult(), votre mise en œuvre de test peut appeler dispatchResult() directement, ce qui vous permet d'obtenir les résultats exacts que vous souhaitez utiliser dans votre test :

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

Le test complet crée le résultat attendu, construit un ActivityResultRegistry de test, le transmet au fragment, déclenche le lanceur d'applications directement ou à l'aide d'autres API de test comme Espresso, puis vérifie les résultats :

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

Créer un contrat personnalisé

Bien que l'élément ActivityResultContracts contienne un certain nombre de classes ActivityResultContract prédéfinies à utiliser, vous pouvez fournir vos propres contrats pour obtenir l'API sécurisée dont vous avez besoin.

Chaque ActivityResultContract nécessite des classes d'entrée et de sortie définies, en utilisant Void comme type d'entrée si vous n'avez pas besoin d'entrée (en Kotlin, utilisez Void? ou Unit).

Chaque contrat doit mettre en œuvre la méthode createIntent(), qui accepte un Context et l'entrée, puis construit l'Intent qui sera utilisé avec startActivityForResult().

Chaque contrat doit également implémenter parseResult(), qui génère la sortie de la propriété resultCode donnée, telle qu'Activity.RESULT_OK ou Activity.RESULT_CANCELED. et Intent.

Les contrats peuvent éventuellement mettre en œuvre getSynchronousResult() s'il est possible de déterminer le résultat pour une entrée donnée sans devoir appeler createIntent(), démarrer l'autre activité et utiliser parseResult() pour créer le résultat.

L'exemple suivant montre comment construire un 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);
    }
}

Si vous n'avez pas besoin de contrat personnalisé, vous pouvez utiliser le contrat StartActivityForResult. Il s'agit d'un contrat générique qui accepte n'importe quel Intent et renvoie un élément ActivityResult, ce qui vous permet d'extraire le resultCode et l'élément Intent dans votre rappel, comme l'illustre l'exemple suivant :

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