Android?startActivityForResult的調(diào)用與封裝詳解
前言
startActivityForResult 可以說是我們常用的一種操作了,用于啟動新頁面并拿到這個頁面返回的數(shù)據(jù),是兩個 Activity 交互的基本操作。
雖然可以通過接口,消息總線,單例池,ViewModel 等多種方法來間接的實現(xiàn)這樣一個功能,但是 startActivityForResult 還是使用最方便的。
目前有哪些方式實現(xiàn) startActivityForResult 的功能呢?
有新老兩種方式,過時的方法是原生Activity/Fragment的 startActivityForResult 方法。另一種方法是 Activity Result API 通過 registerForActivityResult 來注冊回調(diào)。
我們一起看看都是如何使用,使用起來方便嗎?通常我們又都是如何封裝的呢?
一、原生的使用
不管是Activity還是Fragment,我們都可以使用 startActivityForResult
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 120 && resultCode == -1) { toast("接收到返回的數(shù)據(jù):" + data?.getStringExtra("text")) } }
可以看到雖然標(biāo)記過時了,但是 startActivityForResult 這種方法是可以用的,我們一直這么用的,老項目中有很多頁面都是這么定義的。也并沒有什么問題。
不過既然谷歌推薦我們使用 Result Api 我們在以后使用 startActivityForResult 的時候還是推薦使用新的方式。
二、對原生的封裝Ghost
在之前我們使用 startActivityForResult 這種方式的時候,為了更加方便的私有,有一種很流行的方式 Ghost 。
它使用一種 GhostFragment 的空視圖當(dāng)做一次中轉(zhuǎn),這種思路在現(xiàn)在看來已經(jīng)不稀奇了,很多框架如Glide,權(quán)限申請等都是用的這種方案。
它的大致實現(xiàn)流程為:
Activty/Fragment -> add GhostFragment -> onAttach 中 startActivityForResult -> GhostFragment onActivityResult接收結(jié)果 -> callback回調(diào)給Activty/Fragment
總體需要兩個類就可以完成這個邏輯,一個是中轉(zhuǎn)Fragment,一個是管理類:
/** * 封裝Activity Result的API * 使用空Fragemnt的形式調(diào)用startActivityForResult并返回回調(diào) * * Activty/Fragment——>add GhostFragment——>onAttach中startActivityForResult * ——>GhostFragment onActivityResult接收結(jié)果——>callback回調(diào)給Activty/Fragment */ class GhostFragment : Fragment() { private var requestCode = -1 private var intent: Intent? = null private var callback: ((result: Intent?) -> Unit)? = null fun init(requestCode: Int, intent: Intent, callback: ((result: Intent?) -> Unit)) { this.requestCode = requestCode this.intent = intent this.callback = callback } private var activityStarted = false override fun onAttach(activity: Activity) { super.onAttach(activity) if (!activityStarted) { activityStarted = true intent?.let { startActivityForResult(it, requestCode) } } } override fun onAttach(context: Context) { super.onAttach(context) if (!activityStarted) { activityStarted = true intent?.let { startActivityForResult(it, requestCode) } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK && requestCode == this.requestCode) { callback?.let { it1 -> it1(data) } } } override fun onDetach() { super.onDetach() intent = null callback = null } }
/** * 管理GhostFragment用于StartActivityForResult * 啟動的時候添加Fragment 返回的時移除Fragment */ object Ghost { var requestCode = 0 set(value) { field = if (value >= Integer.MAX_VALUE) 1 else value } inline fun launchActivityForResult( starter: FragmentActivity?, intent: Intent, crossinline callback: ((result: Intent?) -> Unit) ) { starter ?: return val fm = starter.supportFragmentManager val fragment = GhostFragment() fragment.init(++requestCode, intent) { result -> callback(result) fm.beginTransaction().remove(fragment).commitAllowingStateLoss() } fm.beginTransaction().add(fragment, GhostFragment::class.java.simpleName) .commitAllowingStateLoss() } }
如此我們就可以使用Kotlin的擴(kuò)展方法來對它進(jìn)行進(jìn)一步的封裝
//真正執(zhí)行AcytivityForResult的方法,使用Ghost的方式執(zhí)行 inline fun <reified T> FragmentActivity.gotoActivityForResult( flag: Int = -1, bundle: Array<out Pair<String, Any?>>? = null, crossinline callback: ((result: Intent?) -> Unit) ) { val intent = Intent(this, T::class.java).apply { if (flag != -1) { this.addFlags(flag) } if (bundle != null) { //調(diào)用自己的擴(kuò)展方法-數(shù)組轉(zhuǎn)Bundle putExtras(bundle.toBundle()!!) } } Ghost.launchActivityForResult(this, intent, callback) }
使用起來就超級簡單了:
gotoActivityForResult<Demo10Activity> { val text = it?.getStringExtra("text") toast("拿到返回數(shù)據(jù):$text") } gotoActivityForResult<Demo10Activity>(bundle = arrayOf("id" to "123", "name" to "zhangsan")) { val text = it?.getStringExtra("text") toast("拿到返回數(shù)據(jù):$text") }
三、Result Api 的使用
其實看Ghost的原來就看得出,他本質(zhì)上還是對 startActivityForResult 的調(diào)用與封裝,還是過期的方法,那么如何使用新的方式,谷歌推薦我們怎么用?
Activity Result API :
它是 Jetpack 的一個組件,這是官方用于替代
startActivityForResult() 和 onActivityResult() 的工具,我們以Activity 1.2.4版本為例:
implementation "androidx.activity:activity-ktx:1.2.4"
那么如何基礎(chǔ)的使用它呢:
private val safLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { val data = result.data?.getStringExtra("text") toast("拿到返回數(shù)據(jù):$data") } } //在方法中使用 safLauncher?.launch(Intent(mActivity, Demo10Activity::class.java))
看起來實現(xiàn)很簡單,但是有幾點要注意,Launcher 的創(chuàng)建需要在onStart生命周期之前,并且回調(diào)是在 Launcher 中處理的。并且 這些 Launcher 并不是只能返回Activity的Result的,還有其他的啟動方式:
StartActivityForResult()
StartIntentSenderForResult()
RequestMultiplePermissions()
RequestPermission()
TakePicturePreview()
TakePicture()
TakeVideo()
PickContact()
GetContent()
GetMultipleContents()
OpenDocument()
OpenMultipleDocuments()
OpenDocumentTree()
CreateDocument()
可以看到這些方式其實對我們來說很多沒必要,在真正的開發(fā)中只有 StartActivityForResult 這一種方式是我們的剛需。
為什么?畢竟現(xiàn)在誰還用這種方式申請權(quán)限,操作多媒體文件。相信大家也都是使用框架來處理了,所以我們這里只對 StartActivityForResult 這一種方式做處理。畢竟這才是我們使用場景最多的,也是我們比較需要的。
經(jīng)過分析,對Result Api的封裝,我們就剩下的兩個重點問題:
- 我們把 Launcher 的回調(diào)能在啟動的方法中觸發(fā)。
- 實現(xiàn) Launcher 在 Activity/Fragment 中的自動注冊。
下面我們就來實現(xiàn)吧。
四、Result Api 的封裝
我們需要做的是:
第一步我們把回調(diào)封裝到launch方法中,并簡化創(chuàng)建的對象方式
第二步我們嘗試自動注冊的功能
4.1 封裝簡化創(chuàng)建方式
首先第一步,我們對 Launcher 對象做一個封裝, 把 ActivityResultCallback 回調(diào)方法在 launch 方法中調(diào)用。
/** * 對Result-Api的封裝,支持各種輸入與輸出,使用泛型定義 */ @SuppressWarnings("unused") public class BaseResultLauncher<I, O> { private final androidx.activity.result.ActivityResultLauncher<I> launcher; private final ActivityResultCaller caller; private ActivityResultCallback<O> callback; private MutableLiveData<O> unprocessedResult; public BaseResultLauncher(@NonNull ActivityResultCaller caller, @NonNull ActivityResultContract<I, O> contract) { this.caller = caller; launcher = caller.registerForActivityResult(contract, (result) -> { if (callback != null) { callback.onActivityResult(result); callback = null; } }); } public void launch(@SuppressLint("UnknownNullness") I input, @NonNull ActivityResultCallback<O> callback) { launch(input, null, callback); } public void launch(@SuppressLint("UnknownNullness") I input, @Nullable ActivityOptionsCompat options, @NonNull ActivityResultCallback<O> callback) { this.callback = callback; launcher.launch(input, options); } }
上門是對Result的基本封裝,由于我們只想要 StartActivityForResult 這一種方式,所以我們定義一個特定的 GetSAFLauncher
/** * 一般我們用這一個-StartActivityForResult 的 Launcher */ class GetSAFLauncher(caller: ActivityResultCaller) : BaseResultLauncher<Intent, ActivityResult>(caller, ActivityResultContracts.StartActivityForResult()) { //封裝另一種Intent的啟動方式 inline fun <reified T> launch( bundle: Array<out Pair<String, Any?>>? = null, @NonNull callback: ActivityResultCallback<ActivityResult> ) { val intent = Intent(commContext(), T::class.java).apply { if (bundle != null) { //調(diào)用自己的擴(kuò)展方法-數(shù)組轉(zhuǎn)Bundle putExtras(bundle.toBundle()!!) } } launch(intent, null, callback) } }
注意這里調(diào)用的是 ActivityResultContracts.StartActivityForResult() 并且泛型的兩個參數(shù)是 Intent 和 ActivityResult。
如果大家想獲取文件,可以使用 GetContent() 泛型的參數(shù)就要變成 String 和 Uri 。由于我們通常不使用這種方式,所以這里不做演示。
封裝第一步之后我們就能這么使用了。
var safLauncher: GetSAFLauncher? = null //其實就是 onCreate 方法 override fun init() { safLauncher = GetSAFLauncher(this@Demo16RecordActivity) } //AFR fun resultTest() { safLauncher?.launch(Intent(mActivity, Demo10Activity::class.java)) { result -> val data = result.data?.getStringExtra("text") toast("拿到返回數(shù)據(jù):$data") } }
//或者使用我們自定義的簡潔方式
fun resultTest() { safLauncher?.launch<Demo10Activity> { result -> val data = result.data?.getStringExtra("text") toast("拿到返回數(shù)據(jù):$data") } safLauncher?.launch<Demo10Activity>(arrayOf("id" to "123", "name" to "zhangsan")) { result -> val data = result.data?.getStringExtra("text") toast("拿到返回數(shù)據(jù):$data") } }
使用下來是不是簡單了很多了,我們只需要創(chuàng)建一個對象就可以了,拿到這個對象調(diào)用launch即可實現(xiàn) startActivityForResult 的功能呢!
4.2 自動注冊/按需注冊
可以看到相比原始的用法,雖然我們現(xiàn)在的用法就簡單了很多,但是我們還是要在oncreate生命周期中創(chuàng)建 Launcher 對象,不然會報錯:
LifecycleOwners must call register before they are STARTED.
那我們有哪些方法處理這個問題?
1)基類定義
我們都已經(jīng)封裝成對象使用了,我們把創(chuàng)建的邏輯定義到BaseActivity/BaseFragment不就行了嗎?
abstract class AbsActivity() : AppCompatActivity(){ protected var safLauncher: GetSAFLauncher? = null ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView() //Result-Api safLauncher = GetSAFLauncher(this) ... } }
這樣不就行了嗎?可以正常使用的。那有人可能說,你這個對象可能用不到,又不是每一個Activity都會用到 Launcher 對象,你這么無腦創(chuàng)建出來消耗內(nèi)存。
有辦法,按需加載!
2).懶加載
懶加載可以吧,我需要的時候就創(chuàng)建。
abstract class AbsActivity() : AppCompatActivity(){ val safLauncher by lazy { GetSAFLauncher(this) } ... }
額,等等,這樣的懶加載貌似是不行的,這在用的時候才初始化,一樣會報錯:
LifecycleOwners must call register before they are STARTED.
我們只能在頁面創(chuàng)建的時候就要明確,這個頁面是否需要這個 Launcher 對象,如果要就要在onCreate中創(chuàng)建對象,如果確定不要 Launcher 對象,那么就不必創(chuàng)建對象。
那我們就這么做:
abstract class AbsActivity() : AppCompatActivity(){ protected var safLauncher: GetSAFLauncher? = null ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView() if (needLauncher()) { //Result-Api safLauncher = GetSAFLauncher(this) } ... } open protected fun needLauncher(): Boolean = false }
我們使用一個flag判斷不就行了嗎?這個頁面如果需要 Launcher 對象,重寫方法返回true就行了。默認(rèn)是不創(chuàng)建這個對象的。
3).Kotlin委托
我們可以使用Kotlin的委托方式,把初始化的代碼和 Launcher 的對象獲取用接口封裝,然后提供對應(yīng)的實現(xiàn)類,不就可以完成按需添加 Launcher 的效果了嗎?
我們定義一個接口,由于邏輯都封裝在了別處,這里就盡量不改動之前的代碼,只是定義初始化和提供對象兩種方法。
/** * 定義是否需要SAFLauncher */ interface ISAFLauncher { fun <T : ActivityResultCaller> T.initLauncher() fun getLauncher(): GetSAFLauncher? }
接著定義這個實現(xiàn)類
class SAFLauncher : ISAFLauncher { private var safLauncher: GetSAFLauncher? = null override fun <T : ActivityResultCaller> T.initLauncher() { safLauncher = GetSAFLauncher(this) } override fun getLauncher(): GetSAFLauncher? = safLauncher }
然后我們就可以使用了:
class Demo16RecordActivity : BaseActivity, ISAFLauncher by SAFLauncher() { //onCreate中直接初始化對象 override fun init() { initLauncher() } //獲取到對象直接用即可,還是之前的幾個方法,沒有變。 fun resultTest() { getLauncher()?.launch<Demo10Activity> { result -> val data = result.data?.getStringExtra("text") toast("拿到返回數(shù)據(jù):$data") } } }
效果都是一樣的:
這樣通過委托的方式,我們就能自己管理初始化,自己隨時獲取到對象調(diào)用launch方法。
如果你當(dāng)前的Activity不需要 startActivityForResult 這種功能,那么你不實現(xiàn)這個接口即可,如果想要 startActivityForResult 的功能,就實現(xiàn)接口委托實現(xiàn),從而實現(xiàn)按需加載的邏輯。
我們再回顧一下 Result Api 需要封裝的兩個痛點與優(yōu)化步驟:
- 第一步我們把回調(diào)封裝到launch方法中,并簡化創(chuàng)建的對象方式
- 第二步我們嘗試自動注冊的功能
同時我們還對一些步驟做了更多的可能性分析,對主動注冊的方式我們有三種方式,(當(dāng)然其實還有更多別的方式來實現(xiàn),我只寫了我認(rèn)為比較簡單方便的幾種方式)。
到此對 Result Api的封裝就此結(jié)束。
總結(jié)
總的來說 Result Api 的封裝其實也不難,使用起來也是很簡單了。如果大家是Kotlin項目我推薦使用委托的方式,如果是Java語言開發(fā)的也可以用flag的方式實現(xiàn)按需加載的邏輯。
而不想使用 Result Api 那么使用原始的 startActivityForResult 也能實現(xiàn),那么我推薦你使用 Ghost 框架,可以更加方便快速的實現(xiàn)返回的功能。
本文對于 Result Api 的封裝也只是限于 startActivityForResult 這一個場景,不過我們這種方式是很方便擴(kuò)展的,如果大家想使用Result Api的方式來操作權(quán)限,文件等,都可以在 BaseResultLauncher 基礎(chǔ)上進(jìn)行擴(kuò)展。
到此這篇關(guān)于Android startActivityForResult的調(diào)用與封裝詳解的文章就介紹到這了,更多相關(guān)Android startActivityForResult內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android自定義View構(gòu)造函數(shù)詳解
這篇文章主要為大家詳細(xì)介紹了Android自定義View構(gòu)造函數(shù),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10Android開發(fā)自學(xué)筆記(四):APP布局下
這篇文章主要介紹了Android開發(fā)自學(xué)筆記(四):APP布局下,本文是上一篇的補充,需要的朋友可以參考下2015-04-04Android編程實現(xiàn)PendingIntent控制多個鬧鐘的方法
這篇文章主要介紹了Android編程實現(xiàn)PendingIntent控制多個鬧鐘的方法,涉及PendingIntent屬性設(shè)置與使用的相關(guān)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-12-12