用ActivityResultContracts代替startActivityForResult

activity之間的傳遞不一定是單向的蛛芥,有時候會需要從跳轉過去的activity往前回傳數(shù)據(jù)(例如,您的應用可啟動相機應用并接收拍攝的照片作為結果)宾濒,過去我們一般采用底層 startActivityForResult()onActivityResult() API腿短,現(xiàn)在谷歌推出了新的Activity Result API為我們解決這類問題。

ActivityResultContracts

基礎用法

  1. ComponentActivityFragment 中鼎兽,使用 registerForActivityResult() API答姥,用于注冊結果回調(diào)。此方法傳入兩個參數(shù)谚咬,ActivityResultContractActivityResultCallback鹦付,該方法返回 ActivityResultLauncher,供您用來啟動另一個 activity择卦。

    val launcher = 
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { res:ActivityResult ? ->
     if (result.resultCode == BACTIVITY_RESULT_CODE){
            val data = result.data?.extras?.getString("extra_data")
            data?.let { toast(it) }
        }
    }
    
  2. 用返回的launcher啟動另一個activity.

    val intent = Intent(this, MainActivity::class.java)
    launcher.launch(intent)
    

    注意:步驟一registerForActivityResult要在oncreate之前調(diào)用(即作為當前activity的屬性聲明)敲长;

    步驟二 launch 要在oncreate之后調(diào)用。

  3. 第二個activity還是照常使用setResult回傳數(shù)據(jù)秉继。

    setResult(BACTIVITY_RESULT_CODE, Intent().putExtra("extra-data", "data"))
    

ActivityResultContract (協(xié)定)

第一步的registerForActivityResult方法傳的第一個參數(shù)是ActivityResultContract祈噪,我們稱之為協(xié)定。里面要約定兩個東西:1. 你啟動launcher時要傳入的對象(本例中為intent)尚辑;2. 返回到當前頁面時帶回來的對象(本例中為ActivityResult)辑鲤。

第二個參數(shù)就是帶著你約定好的對象的回調(diào)了。

示例中用的約定是:ActivityResultContracts.StartActivityForResult()杠茬,這是系統(tǒng)提供的月褥,意思是傳入Intent,返回ActivityResult。

除此之外瓢喉,系統(tǒng)還提供了很多宁赤,如ActivityResultContracts.GetContent(),傳入string,返回UrI,一般是用來獲取系統(tǒng)資源用的栓票,像傳入"image/*",然后返回圖片URI{content://} 之類的决左。

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

我們用Input<I>表示傳入的約定,Output<O>表示回傳回來的約定走贪。協(xié)定就可以抽象成:

public abstract class ActivityResultContract<I, O> {
    /**
     * 創(chuàng)建Intent佛猛,不管你約定的輸入是啥,要跳轉最后還是用的Intent跳厉斟,所以在這得自己創(chuàng)建Intent
     **/
    public abstract Intent createIntent(Context context,I input);
   
    /**
     * 解析回調(diào)的結果
     **/
    public abstract O parseResult(int resultCode, @Nullable Intent intent);
    
    /**
     * 可選
     **/
    public SynchronousResult<O> getSynchronousResult(Context context,I input) {
        return null;
    }
}

先來看示例中用的這個協(xié)定 - StartActivityForResult

官方提供的協(xié)定

public static final class StartActivityForResult extends ActivityResultContract<Intent, ActivityResult> {

    @NonNull
    @Override
    public Intent createIntent(@NonNull Context context, @NonNull Intent input) {
        return input;
    }

    @NonNull
    @Override
    public ActivityResult parseResult(
            int resultCode, @Nullable Intent intent) {
        return new ActivityResult(resultCode, intent);
    }
}

很簡單挚躯,傳入的就是intent,返回的就是resultCode + intent的封裝類擦秽;

再看下剛剛提到的傳圖片的那個:

public static class GetContent extends ActivityResultContract<String, Uri> {

    //CallSuper是提醒繼承該類的話應該重寫該方法,上面那個是final不能重寫,所以沒有
    @CallSuper
    @NonNull
    @Override
    public Intent createIntent(@NonNull Context context, @NonNull String input) {
        return new Intent(Intent.ACTION_GET_CONTENT)
                .addCategory(Intent.CATEGORY_OPENABLE)
                .setType(input);
    }

    @Nullable
    @Override
    public final SynchronousResult<Uri> getSynchronousResult(@NonNull Context context,
            @NonNull String input) {
        return null;
    }

    @Nullable
    @Override
    public final Uri parseResult(int resultCode, @Nullable Intent intent) {
        if (intent == null || resultCode != Activity.RESULT_OK) return null;
        return intent.getData();
    }
}

傳入特定的Intent感挥,返回的結果先解析下再返回回去缩搅。

理解了這個協(xié)定后,我們也可以自己約定協(xié)定触幼。

創(chuàng)建自定義協(xié)定

比如說一個時間選擇的Activity硼瓣,它的返回到上一頁面的時候要攜帶當前界面選擇的時間。因為進入到該界面的時候Intent是固定的置谦,所以Input可以傳入void堂鲤,意思是不用傳。

class TimeResultContract : ActivityResultContract<Unit,String>(){
    override fun createIntent(context: Context, input: Unit?): Intent {
        return Intent(context,TimeSelectActivity::class.java)
    }

    override fun parseResult(resultCode: Int, intent: Intent?): String {
        if (resultCode != RESULT_OK){
            return "error"
        }
        return intent?.getStringExtra(TimeSelectActivity_EXTRA_DATE).toString()
    }

}

用的時候只需要在需要跳轉的activity中聲明即可媒峡。

val timeLauncher = registerForActivityResult(TimeResultContract()){ time ->
    toast(time)
    viewBinding.tvTime.text = time
}

相比以前需要把requestCode判斷的邏輯寫到各個Activity中的寫法瘟栖,這樣封裝起來看起來是不是干凈多了?

如果你還想把registerForActivityResult這個方法也提到activity外面去谅阿,也是可以做到的半哟。

進階

在單獨的類中接收 activity 結果

之所以能在ComponentActivityFragment 類中直接調(diào)用registerForActivityResult()方法是因為它們實現(xiàn)了ActivityResultCaller接口。如果你想在未實現(xiàn)ActivityResultCaller接口的類中獲取launcher签餐,那么就需要用到ActivityResultRegistry 類了寓涨。

例如,您可能需要實現(xiàn)一個 LifecycleObserver氯檐,用于處理協(xié)定的注冊和啟動器的啟動:

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

源碼解析

從上面的用法可以看出戒良,實現(xiàn)這個功能的核心其實是傳進來的registry:ActivityResultRegistry。 而在ComponentActivityFragment中都維護了有一個registry,以activity為例:

private final ActivityResultRegistry mActivityResultRegistry = new ActivityResultRegistry() {

    @Override
    public <I, O> void onLaunch( final int requestCode, @NonNull ActivityResultContract<I, O> contract,
            I input, @Nullable ActivityOptionsCompat options) {
        ComponentActivity activity = ComponentActivity.this;

        // Immediate result path
        final ActivityResultContract.SynchronousResult<O> synchronousResult =
                contract.getSynchronousResult(activity, input);
        if (synchronousResult != null) {
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    dispatchResult(requestCode, synchronousResult.getValue());
                }
            });
            return;
        }

        // Start activity path
        Intent intent = contract.createIntent(activity, input);
        Bundle optionsBundle = null;
        // If there are any extras, we should defensively set the classLoader
        if (intent.getExtras() != null && intent.getExtras().getClassLoader() == null) {
            intent.setExtrasClassLoader(activity.getClassLoader());
        }
        if (intent.hasExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE)) {
            optionsBundle = intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
            intent.removeExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
        } else if (options != null) {
            optionsBundle = options.toBundle();
        }
        if (ACTION_REQUEST_PERMISSIONS.equals(intent.getAction())) {

            // requestPermissions path
            String[] permissions = intent.getStringArrayExtra(EXTRA_PERMISSIONS);

            if (permissions == null) {
                permissions = new String[0];
            }

            ActivityCompat.requestPermissions(activity, permissions, requestCode);
        } else if (ACTION_INTENT_SENDER_REQUEST.equals(intent.getAction())) {
            IntentSenderRequest request =
                    intent.getParcelableExtra(EXTRA_INTENT_SENDER_REQUEST);
            try {
                // startIntentSenderForResult path
                ActivityCompat.startIntentSenderForResult(activity, request.getIntentSender(),
                        requestCode, request.getFillInIntent(), request.getFlagsMask(),
                        request.getFlagsValues(), 0, optionsBundle);
            } catch (final IntentSender.SendIntentException e) {
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        dispatchResult(requestCode, RESULT_CANCELED,
                                new Intent().setAction(ACTION_INTENT_SENDER_REQUEST)
                                        .putExtra(EXTRA_SEND_INTENT_EXCEPTION, e));
                    }
                });
            }
        } else {
            // startActivityForResult path
            ActivityCompat.startActivityForResult(activity, intent, requestCode, optionsBundle);
        }
    }
};

然后實際調(diào)用的是registry.register()方法(這是個抽象類冠摄,該方法寫在抽象類中)

public final <I, O> ActivityResultLauncher<I> register(
        @NonNull final String key,
        @NonNull final LifecycleOwner lifecycleOwner,
        @NonNull final ActivityResultContract<I, O> contract,
        @NonNull final ActivityResultCallback<O> callback) {

    Lifecycle lifecycle = lifecycleOwner.getLifecycle();

    if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
        throw new IllegalStateException("LifecycleOwner " + lifecycleOwner + " is "
                + "attempting to register while current state is "
                + lifecycle.getCurrentState() + ". LifecycleOwners must call register before "
                + "they are STARTED.");
    }

    final int requestCode = registerKey(key);
    LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
    if (lifecycleContainer == null) {
        lifecycleContainer = new LifecycleContainer(lifecycle);
    }
    LifecycleEventObserver observer = new LifecycleEventObserver() {
        @Override
        public void onStateChanged(
                @NonNull LifecycleOwner lifecycleOwner,
                @NonNull Lifecycle.Event event) {
            if (Lifecycle.Event.ON_START.equals(event)) {
                mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));
                if (mParsedPendingResults.containsKey(key)) {
                    @SuppressWarnings("unchecked")
                    final O parsedPendingResult = (O) mParsedPendingResults.get(key);
                    mParsedPendingResults.remove(key);
                    callback.onActivityResult(parsedPendingResult);
                }
                final ActivityResult pendingResult = mPendingResults.getParcelable(key);
                if (pendingResult != null) {
                    mPendingResults.remove(key);
                    callback.onActivityResult(contract.parseResult(
                            pendingResult.getResultCode(),
                            pendingResult.getData()));
                }
            } else if (Lifecycle.Event.ON_STOP.equals(event)) {
                mKeyToCallback.remove(key);
            } else if (Lifecycle.Event.ON_DESTROY.equals(event)) {
                unregister(key);
            }
        }
    };
    lifecycleContainer.addObserver(observer);
    mKeyToLifecycleContainers.put(key, lifecycleContainer);

    return new ActivityResultLauncher<I>() {
        @Override
        public void launch(I input, @Nullable ActivityOptionsCompat options) {
            mLaunchedKeys.add(key);
            onLaunch(requestCode, contract, input, options);
        }

        @Override
        public void unregister() {
            ActivityResultRegistry.this.unregister(key);
        }

        @NonNull
        @Override
        public ActivityResultContract<I, ?> getContract() {
            return contract;
        }
    };
}

可以看到這里對生命周期所在狀態(tài)進行了很多判斷糯崎,可以防止聲明周期外的內(nèi)存泄漏的問題。

調(diào)用launch方法的時候實際走的是onLaunch()耗拓,也就是第一段代碼里面的內(nèi)容拇颅,最終調(diào)用的還是startActivityForResult方法,所以這個新的用法還是基于startActivityForResult的乔询,但是卻很好的將回傳的數(shù)據(jù)的判斷相關功能封裝到了協(xié)定類中樟插,以后就算有多個回傳數(shù)據(jù),用起來也會很清晰竿刁,代碼就不會臃腫到onActivityResult中了黄锤。

示例-獲取權限

request_permission.setOnClickListener {
    requestPermission.launch(permission.BLUETOOTH)
}

request_multiple_permission.setOnClickListener {
    requestMultiplePermissions.launch(
        arrayOf(
            permission.BLUETOOTH,
            permission.NFC,
            permission.ACCESS_FINE_LOCATION
        )
    )
}

// Request permission contract
private val requestPermission =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
        // Do something if permission granted
        if (isGranted) toast("Permission is granted")
        else toast("Permission is denied")
    }

// Request multiple permissions contract
private val requestMultiplePermissions =
    registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions : Map<String, Boolean> ->
        // Do something if some permissions granted or denied
        permissions.entries.forEach {
            // Do checking here
        }                                                                             
}

相較原來的獲取權限的寫法清晰多了。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末食拜,一起剝皮案震驚了整個濱河市鸵熟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌负甸,老刑警劉巖流强,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痹届,死亡現(xiàn)場離奇詭異,居然都是意外死亡打月,警方通過查閱死者的電腦和手機队腐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奏篙,“玉大人柴淘,你說我怎么就攤上這事∶赝ǎ” “怎么了为严?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長肺稀。 經(jīng)常有香客問我第股,道長,這世上最難降的妖魔是什么盹靴? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任炸茧,我火速辦了婚禮,結果婚禮上稿静,老公的妹妹穿的比我還像新娘梭冠。我一直安慰自己,他們只是感情好改备,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布控漠。 她就那樣靜靜地躺著,像睡著了一般悬钳。 火紅的嫁衣襯著肌膚如雪盐捷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天默勾,我揣著相機與錄音碉渡,去河邊找鬼。 笑死母剥,一個胖子當著我的面吹牛滞诺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播环疼,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼习霹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了炫隶?” 一聲冷哼從身側響起淋叶,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伪阶,沒想到半個月后煞檩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體处嫌,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年形娇,在試婚紗的時候發(fā)現(xiàn)自己被綠了锰霜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筹误。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡桐早,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出厨剪,到底是詐尸還是另有隱情哄酝,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布祷膳,位于F島的核電站陶衅,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏直晨。R本人自食惡果不足惜搀军,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望勇皇。 院中可真熱鬧罩句,春花似錦、人聲如沸敛摘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兄淫。三九已至屯远,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捕虽,已是汗流浹背慨丐。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留泄私,地道東北人房揭。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像挖滤,于是被迫代替她去往敵國和親崩溪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 傳統(tǒng)藍牙適用于較為耗電的操作,其中包括 Android 設備之間的流式傳輸和通信等惧盹。針對具有低功耗要求的藍牙設備乳幸,...
    路人甲_47a3閱讀 2,095評論 1 1
  • 表情是什么瞪讼,我認為表情就是表現(xiàn)出來的情緒。表情可以傳達很多信息粹断。高興了當然就笑了符欠,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 125,009評論 2 7
  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者瓶埋,不喜歡去冒險希柿,但是人生放棄了冒險,也就放棄了無數(shù)的可能养筒。 ...
    yichen大刀閱讀 6,049評論 0 4