startActivityForResult的新解決辦法:ActivityResultContract

轉(zhuǎn)自:Google 更新:Android開發(fā)者是時候丟掉 onActivityResult 了 庄蹋!
但是這邊文章的相關api是alpha時的api,現(xiàn)在有點方法名已經(jīng)變了哦


在學習Jetpack相關庫的時候,在ComponentActivity(屬于:androidx.activity:activity:1.2.0-alpha05)里看到了ActivityResultRegistry這個類,剛好在知乎又看到過類似的文章,于是來學習一下.

但凡涉及到啟動新Activity,并獲取返回值,或者調(diào)用相機拍照,那一定會逃不過startActivityForResult 和 onActivityResult的,在有些業(yè)務情景中,這個模式很大的制約了代碼的設計,谷歌在Activity 1.2.0-alpha02 和 Fragment 1.3.0-alpha02 開始,提供了新的Result API,讓我們能更加優(yōu)雅的處理onActivityResult,已到達:減少樣板代碼,解耦,靈活,易測試的目的

1.傳統(tǒng)的startActivityForResult

最簡單的場景: MainActivity跳到SecondActivity,SecondActivity傳值回來,代碼如下:

const val TAG = "ActivityResultContracts"

class MainActivity : ComponentActivity(R.layout.activity_main) {
    private val REQUEST_CODE = 1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        button2.setOnClickListener {
            jump()
        }
    }

    //<editor-fold desc="頁面跳轉(zhuǎn) 傳統(tǒng)寫法">
    private fun jump() {
        startActivityForResult(
            Intent(this, SecondActivity::class.java),
            REQUEST_CODE
        )
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE) {
            Toast.makeText(
                this,
                data?.getStringExtra("value") ?: "no return data",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

    //</editor-fold>
}

class SecondActivity : AppCompatActivity(R.layout.activity_second) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        back.setOnClickListener {
            setResult(Activity.RESULT_OK, Intent().putExtra("value", "I am back !"))
            finish()
        }
    }
}

當我們集成了androidx.activity:activity:1.2.0-alpha05后,startActivityForResult方法已經(jīng)被標記為了@Deprecated

插播:
新的AppCompatActivity以及ComponentActivity,支持構(gòu)造函數(shù)中傳入layotuId了,不用再setContentView()了

基本的流程是:

  • 定義一個 REQUEST_CODE ,同一頁面有多個時殖妇,保證不重復
  • 調(diào)用 startActivityForResult
  • 在 onActivityResult 中接收回調(diào)恩敌,并判斷 requestCode狼牺,resultCode
    而且上述代碼,都必須寫在視圖控制器(Activity/Fragment)里,也就造成了不容易測試等問題,

但是長久以來,我們也只有這一個選擇,所以也很少看到有人抱怨 onActivityResult词裤。
Google 工程師為我們改進了這一問題演训。就推出了新的 Activity Result API 弟孟。

2.Activity Result API

    //<editor-fold desc="頁面跳轉(zhuǎn) 新的寫法">
    private val startActivity =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            Log.d(TAG, "activityResult -> $it")
            Log.d(TAG, "activityResult -> ${it.data?.getStringExtra("value")}")
            textView.text = it.data?.getStringExtra("value")
        }

    fun jumpV2() {
        startActivity.launch(Intent(this, SecondActivity::class.java))
    }

    //</editor-fold>

P.S.
registerForActivityResult方法,在之前較早的版本總是叫做prepareCall的

可以看到,主要就是2個方法: registerForActivityResult 和 launch

下面來詳細解讀一下 這兩個方法:

    @NonNull
    @Override
    public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull ActivityResultContract<I, O> contract,
            @NonNull ActivityResultCallback<O> callback) {
        return registerForActivityResult(contract, mActivityResultRegistry, callback);

registerForActivityResult方法接收兩個參數(shù),ActivityResultContract 和 ActivityResultCallback 样悟,返回值是 ActivityResultLauncher 。這幾個名字取得都很好,見名知意窟她。

ActivityResultContract

ActivityResultContract 可以理解為一種協(xié)議陈症,它是一個抽象類,提供了兩個能力震糖,createIntent 和 parseResult 录肯。

這兩個能力放到啟動 Activity 中就很好理解了,createIntent 負責為 startActivityForResult 提供 Intent 吊说,parseResult 負責處理 onActivityResult 中獲取的結(jié)果论咏。

上面的例子中,registerForActivityResult() 方法傳入的協(xié)議實現(xiàn)類是 StartActivityForResult 颁井。

它是 ActivityResultContracts 類中的靜態(tài)內(nèi)部類厅贪。除了 StartActivityForResult 之外,官方還默認提供了 RequestPermissions 雅宾,Dial 养涮,RequestPermission ,TakePicture眉抬,它們都是 ActivityResultContract 的實現(xiàn)類,而且都位于ActivityResultContracts 這個final類里面

所以贯吓,除了可以簡化 startActivityForResult ,權(quán)限請求蜀变,撥打電話悄谐,拍照,都可以通過 Activity Result API 得到了簡化库北。

除了使用官方默認提供的這些之外爬舰,我們還可以自己實現(xiàn) ActivityResultContract,在后面的代碼中會進行演示贤惯。

ActivityResultCallback

public interface ActivityResultCallback<O> {

    /**
     * Called when result is available
     */
    void onActivityResult(@SuppressLint("UnknownNullness") O result);
}

這個就很容易立即,是結(jié)果的回調(diào)接口
需要注意的是: registerForActivityResult()方法的泛型現(xiàn)在,這里的返回值result即泛型里的O (output)泛型,一定是類型安全的.

  • StartActivityForResult --> ActivityResult
  • TakePicture --> Bitmap
  • Dial/RequestPermission --> Boolean
  • RequestPermissions--> Map
    具體的,可以去ActivityResultContracts類里面查看各個協(xié)議的具體參數(shù)類型

ActivityResultLauncher

registerForActivityResult的返回值,通過調(diào)用launch()方法,執(zhí)行業(yè)務,最終會調(diào)用ActivityResultRegistry.register()方法,具體的源碼這里就不分析了洼专,后面原博會單獨寫一篇源碼解析。

大致流程是: 自動生成 requestCode孵构,注冊回調(diào)并存儲起來屁商,綁定生命周期,當收到 Lifecycle.Event.ON_DESTROY 事件時颈墅,自動解綁注冊蜡镶。

代替 startActivityForResult() 的就是 ActivityResultLauncher.launch()方法,最后會調(diào)用到 ActivityResultRegistry.invoke() 方法恤筛,如下所示:

Intent intent = contract.createIntent(activity, input);
                    if ("androidx.activity.result.contract.action.REQUEST_PERMISSIONS".equals(intent.getAction())) {
                        // handle request permissions
                    } else if ("androidx.activity.result.contract.action.INTENT_SENDER_REQUEST".equals(intent.getAction())) {
                       // handle intentSender
                    } else {
                        Bundle optionsBundle = null;
                        if (intent.hasExtra("androidx.activity.result.contract.extra.ACTIVITY_OPTIONS_BUNDLE")) {
                            optionsBundle = intent.getBundleExtra("androidx.activity.result.contract.extra.ACTIVITY_OPTIONS_BUNDLE");
                        } else if (options != null) {
                            optionsBundle = options.toBundle();
                        }

                        ActivityCompat.startActivityForResult(activity, intent, requestCode, optionsBundle);
                    }

可以看到,最終調(diào)用的是 ActivityCompat.startActivityForResult();

中間那一塊處理 request permissions 的我給掐掉了官还。這樣看起來看清晰。本來準備單獨水一篇源碼解析的毒坛,這馬上核心源碼都講完了望伦。

前面展示過了 startActivityForResult() 林说,再來展示一下權(quán)限請求。


    private val permissionRequest =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) {
            Log.d(TAG, "permissionResult -> $it")
            textView.text = "CAMERA result -> $it"
        }

    fun jumpV3() {
        permissionRequest.launch(Manifest.permission.CAMERA)
    }


其余權(quán)限的請求,在這里就不展示了

3.如何自定義返回值 屯伞?

前面提到的都是系統(tǒng)預置的協(xié)議(ActivityResultContract)腿箩,輸入值,返回值類型也都是固定的。那么劣摇,如何返回自定義類型的值呢珠移?其實也很簡單,自定義 ActivityResultContract,指明業(yè)務所需要的泛型就ok了

我們以TakePicturePreview為例,輸入值是Void 默認返回值是Bitmap,現(xiàn)在假如我們需要返回Drawable

    private class TakeDrawable(val context: Context) : ActivityResultContract<Void, Drawable>() {
        override fun createIntent(context: Context, input: Void?): Intent {
            return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        }

        override fun parseResult(resultCode: Int, intent: Intent?): Drawable? {
            if (null == intent || resultCode != Activity.RESULT_OK)
                return null
            if (intent == null || resultCode != Activity.RESULT_OK) return null;
            val bitmap = intent.getParcelableExtra<Bitmap>("data")
            return BitmapDrawable(context.resources, bitmap);
        }

    }

    private val picture =
        registerForActivityResult(TakeDrawable(this)) {
            Log.d(TAG, "picture -> $it")
            imageView.setImageDrawable(it)
        }


    fun takePic() {
        picture.launch(null);
    }

大部分代碼參照TakePicturePreview的邏輯,只需要在parseResult里面,使用構(gòu)造方法傳入的Context,把bimap對象轉(zhuǎn)換為BitmapDrawable即可.
這樣就可以調(diào)用系統(tǒng)相機拍照并在結(jié)果回調(diào)中拿到 Drawable 對象了末融。

4.說好的解耦呢 钧惧?

有時候我們可能會在結(jié)果回調(diào)中進行一些復雜的處理操作,無論是之前的 onActivityResult() 還是上面內(nèi)部類的寫法勾习,都是直接耦合在視圖控制器中的浓瞪。

通過新的 Activity Result API,我們還可以單獨的類中處理結(jié)果回調(diào)语卤,真正做到 單一職責 追逮。

其實 Activity Result API 的核心操作都是通過 ActivityResultRegistry 來完成的,ComponentActivity 中包含了一個 ActivityResultRegistry 對象

    @NonNull
    @Override
    public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull ActivityResultContract<I, O> contract,
            @NonNull ActivityResultCallback<O> callback) {
        return registerForActivityResult(contract, mActivityResultRegistry, callback);
    }

    @NonNull
    @Override
    public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultRegistry registry,
            @NonNull final ActivityResultCallback<O> callback) {
        return registry.register(
                "activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
    }

    /**
     * Get the {@link ActivityResultRegistry} associated with this activity.
     *
     * @return the {@link ActivityResultRegistry}
     */
    @NonNull
    @Override
    public final ActivityResultRegistry getActivityResultRegistry() {
        return mActivityResultRegistry;
    }

通過新的 Activity Result API粹舵,我們還可以單獨的類中處理結(jié)果回調(diào)钮孵,真正做到 單一職責 。

其實 Activity Result API 的核心操作都是通過 ActivityResultRegistry 來完成的眼滤,ComponentActivity 中包含了一個 ActivityResultRegistry 對象

我們可以看到,registerForActivityResult(,,_)方法的第二個參數(shù),就是ActivityResultRegistry,并且ComponentActivity還提供了相應的get方法暴露ActivityResultRegistry

現(xiàn)在要脫離 Activity 完成操作巴席,就需要把ActivityResultRegistry 提供給外部,用來來進行結(jié)果回調(diào)的注冊工作。同時诅需,我們一般通過實現(xiàn) LifecycleObserver 接口漾唉,綁定個 LifecycleOwner 來進行自動解綁注冊。

class TakePicPreviewObserver(
    val activityResultRegistry: ActivityResultRegistry,
    val onResult: (Bitmap) -> Unit
) : DefaultLifecycleObserver {
    lateinit var takePhotoLauncher :ActivityResultLauncher<Void>
    override fun onCreate(owner: LifecycleOwner) {
        Log.d("TakePicObsver","onCreate")
        takePhotoLauncher = activityResultRegistry.register(
            "key",
            ActivityResultContracts.TakePicturePreview(),
            onResult
        )
    }
    fun takePicture() {
        takePhotoLauncher.launch(null)
    }

    override fun onDestroy(owner: LifecycleOwner) {
        Log.d("TakePicObsver","onDestroy")
        takePhotoLauncher.unregister()
    }
}

插播:對DefaultLifycycleObserver的說明

DefaultLifecyclerObserver'androidx.lifecycle:lifecycle-common-java8:2.3.0-alpha03'包里的,也是唯一個文件,
繼承自FullLifecyclerObserver(androidx.lifecycle:lifecycle-common:2.3.0-alpha03包內(nèi)的),
而且兩個類完全一樣,而且DefaultLifecyclerObserver
@SuppressWarnings("unused") 表示該屬性在方法或類中沒有使用堰塌。添加此注解可以去除屬性上的黃色警告U孕獭!场刑!
但是FullLifecycleObserver不是public的是package private(即:我們常說的包保護)的,因此,
外部類是不能直接實現(xiàn)它的,所以需要借助DefaultLifecyclerObserver,來使用相應方法


/**
 * Callback interface for listening to {@link LifecycleOwner} state changes.
 * <p>
 * If you use Java 8 language, <b>always</b> prefer it over annotations.
 * <p>
 * If a class implements both this interface and {@link LifecycleEventObserver}, then
 * methods of {@code DefaultLifecycleObserver} will be called first, and then followed by the call
 * of {@link LifecycleEventObserver#onStateChanged(LifecycleOwner, Lifecycle.Event)}
 * <p>
 * If a class implements this interface and in the same time uses {@link OnLifecycleEvent}, then
 * annotations will be ignored.
 */

上面的注釋也已經(jīng)指明,DefaultLifecyclerObserver是LifecycleOwner的一個回調(diào)接口,
如果使用java8 作為語言環(huán)境,那么使用這個DefaultLifecyclerObserver是優(yōu)于使用注解處理生命周期回調(diào)的

下面翻譯一下上方注釋的其余部分:

如果某個類實現(xiàn)了即實現(xiàn)了{@link DefaultLifecyclerObserver}  又實現(xiàn)了{@link LifecycleEventObserver}接口,
那么 {@link DefaultLifecyclerObserver}的實現(xiàn)方法,優(yōu)先調(diào)用,然后會回調(diào) {@link LifecycleEventObserver#onStateChanged(LifecycleOwner, Lifecycle.Event)}
方法

如果某個類實現(xiàn)了{@link DefaultLifecyclerObserver}接口,同時,又使用了 {@link OnLifecycleEvent}的注解,
那么注解會被忽略,不會被調(diào)用

再附上 LifecycleObserver FullLifecycleObserver LifecycleEventObserver DefaultLifecyclerObserver
的層級關系

LifecycleObserver
   ?LifecycleEventObserver
   ?FullLifecycleObserver(package private)
        ?DefaultLifecyclerObserver

5.再玩點花出來 般此?結(jié)合LiveData,進行數(shù)據(jù)觀察

在 Github 上看到了一些花式寫法,和大家分享一下牵现。

class TakePicPreviewLiveData(
    val activityResultRegistry: ActivityResultRegistry
) : LiveData<Bitmap>() {
    private  lateinit var takePhotoLauncher: ActivityResultLauncher<Void>
    override fun onActive() {
        super.onActive()
        Log.d("TakePicLiveData", "onActive")
        takePhotoLauncher = activityResultRegistry.register(
            "key",
            ActivityResultContracts.TakePicturePreview()
        ) {
            Log.d("TakePicLiveData", "onActive callback thread: ${Thread.currentThread().name}")
            value = it
        }
    }
    fun takePicture() {
        takePhotoLauncher.launch(null)
    }
    override fun onInactive() {
        super.onInactive()
        Log.d("TakePicLiveData", "onInactive")
        //takePhotoLauncher.unregister()
    }
}

但是,又一個問題出現(xiàn)了,目前不知道如何解決:可以看到,在onInactive()這個LiveData的生命周期里,
我注釋掉了一句代碼takePhotoLauncher.unregister()
這么做的原因是,當我們喚起相機,進行拍照時,我們的Activity或Fragment作為LifecycleOwener,會進入onPause()
生命周期,同時onInactive(),會在生命周期不是{@link Lifecycle.State#STARTED} 時 {@link Lifecycle.State#RESUMED}
被回調(diào),也就是說,我們喚起相機拍照時,onInactive會被回調(diào),如果我們在這里對ActivityResultLauncher進行unregister(),
那么,我們就拿不到返回的結(jié)果了,也就無法通過LiveData進一步把數(shù)據(jù)通知出去了.

對于這個問題,我暫時沒有想到好的解決辦法,
只有一個想法,就是我們在繼承LiveData的同時,實現(xiàn)DefaultLifecycleObserver,就想之前寫的TakePicPreviewObserver一樣,
在DefaultLifecycleObserver#onDestroy 里進行 unregister()

至此,關于Activity Result API的學習,就暫告一段落,希望對你有所幫助...

本篇文字寫于: 2020/6/8

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末铐懊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瞎疼,更是在濱河造成了極大的恐慌科乎,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贼急,死亡現(xiàn)場離奇詭異茅茂,居然都是意外死亡捏萍,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門玉吁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來照弥,“玉大人腻异,你說我怎么就攤上這事进副。” “怎么了悔常?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵影斑,是天一觀的道長。 經(jīng)常有香客問我机打,道長矫户,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任残邀,我火速辦了婚禮皆辽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘芥挣。我一直安慰自己驱闷,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布空免。 她就那樣靜靜地躺著空另,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蹋砚。 梳的紋絲不亂的頭發(fā)上扼菠,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音坝咐,去河邊找鬼循榆。 笑死,一個胖子當著我的面吹牛墨坚,可吹牛的內(nèi)容都是我干的秧饮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼框杜,長吁一口氣:“原來是場噩夢啊……” “哼浦楣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起咪辱,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤振劳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后油狂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體历恐,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡寸癌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了弱贼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒸苇。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吮旅,靈堂內(nèi)的尸體忽然破棺而出溪烤,到底是詐尸還是另有隱情,我是刑警寧澤庇勃,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布檬嘀,位于F島的核電站,受9級特大地震影響责嚷,放射性物質(zhì)發(fā)生泄漏鸳兽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一罕拂、第九天 我趴在偏房一處隱蔽的房頂上張望揍异。 院中可真熱鬧,春花似錦爆班、人聲如沸衷掷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棍鳖。三九已至,卻和暖如春碗旅,著一層夾襖步出監(jiān)牢的瞬間渡处,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工祟辟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留医瘫,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓旧困,卻偏偏與公主長得像醇份,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子吼具,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345