首頁 > 軟體

Android startActivityForResult的呼叫與封裝詳解

2023-03-23 22:04:19

前言

startActivityForResult 可以說是我們常用的一種操作了,用於啟動新頁面並拿到這個頁面返回的資料,是兩個 Activity 互動的基本操作。

雖然可以通過介面,訊息匯流排,單例池,ViewModel 等多種方法來間接的實現這樣一個功能,但是 startActivityForResult 還是使用最方便的。

目前有哪些方式實現 startActivityForResult 的功能呢?

有新老兩種方式,過時的方法是原生Activity/Fragment的 startActivityForResult 方法。另一種方法是 Activity Result API 通過 registerForActivityResult 來註冊回撥。

我們一起看看都是如何使用,使用起來方便嗎?通常我們又都是如何封裝的呢?

一、原生的使用

不管是Activity還是Fragment,我們都可以使用 startActivityForResult

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == 120 && resultCode == -1) {
            toast("接收到返回的資料:" + data?.getStringExtra("text"))
        }
    }

可以看到雖然標記過時了,但是 startActivityForResult 這種方法是可以用的,我們一直這麼用的,老專案中有很多頁面都是這麼定義的。也並沒有什麼問題。

不過既然谷歌推薦我們使用 Result Api 我們在以後使用 startActivityForResult 的時候還是推薦使用新的方式。

二、對原生的封裝Ghost

在之前我們使用 startActivityForResult 這種方式的時候,為了更加方便的私有,有一種很流行的方式 Ghost 。

它使用一種 GhostFragment 的空檢視當做一次中轉,這種思路在現在看來已經不稀奇了,很多框架如Glide,許可權申請等都是用的這種方案。

它的大致實現流程為:

Activty/Fragment -> add GhostFragment -> onAttach 中 startActivityForResult -> GhostFragment onActivityResult接收結果 -> callback回撥給Activty/Fragment

總體需要兩個類就可以完成這個邏輯,一個是中轉Fragment,一個是管理類:

/**
 * 封裝Activity Result的API
 * 使用空Fragemnt的形式呼叫startActivityForResult並返回回撥
 *
 * Activty/Fragment——>add GhostFragment——>onAttach中startActivityForResult
 * ——>GhostFragment onActivityResult接收結果——>callback回撥給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的擴充套件方法來對它進行進一步的封裝

//真正執行AcytivityForResult的方法,使用Ghost的方式執行
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) {
            //呼叫自己的擴充套件方法-陣列轉Bundle
            putExtras(bundle.toBundle()!!)
        }
    }
    Ghost.launchActivityForResult(this, intent, callback)
}

使用起來就超級簡單了:

    gotoActivityForResult<Demo10Activity> {
        val text = it?.getStringExtra("text")
        toast("拿到返回資料:$text")
    }

    gotoActivityForResult<Demo10Activity>(bundle = arrayOf("id" to "123", "name" to "zhangsan")) {
        val text = it?.getStringExtra("text")
        toast("拿到返回資料:$text")
    }

三、Result Api 的使用

其實看Ghost的原來就看得出,他本質上還是對 startActivityForResult 的呼叫與封裝,還是過期的方法,那麼如何使用新的方式,谷歌推薦我們怎麼用?

Activity Result API :

它是 Jetpack 的一個元件,這是官方用於替代 startActivityForResult() 和 onActivityResult() 的工具,我們以Activity 1.2.4版本為例:

implementation "androidx.activity:activity-ktx:1.2.4"

那麼如何基礎的使用它呢:

  
    private val safLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == RESULT_OK) {
            val data = result.data?.getStringExtra("text")
            toast("拿到返回資料:$data")
        }
    }
    
    //在方法中使用
    safLauncher?.launch(Intent(mActivity, Demo10Activity::class.java))

看起來實現很簡單,但是有幾點要注意,Launcher 的建立需要在onStart生命週期之前,並且回撥是在 Launcher 中處理的。並且 這些 Launcher 並不是只能返回Activity的Result的,還有其他的啟動方式:

StartActivityForResult()
StartIntentSenderForResult()
RequestMultiplePermissions()
RequestPermission()
TakePicturePreview()
TakePicture()
TakeVideo()
PickContact()
GetContent()
GetMultipleContents()
OpenDocument()
OpenMultipleDocuments()
OpenDocumentTree()
CreateDocument()

可以看到這些方式其實對我們來說很多沒必要,在真正的開發中只有 StartActivityForResult 這一種方式是我們的剛需。

為什麼?畢竟現在誰還用這種方式申請許可權,操作多媒體檔案。相信大家也都是使用框架來處理了,所以我們這裡只對 StartActivityForResult 這一種方式做處理。畢竟這才是我們使用場景最多的,也是我們比較需要的。

經過分析,對Result Api的封裝,我們就剩下的兩個重點問題:

  • 我們把 Launcher 的回撥能在啟動的方法中觸發。
  • 實現 Launcher 在 Activity/Fragment 中的自動註冊。

下面我們就來實現吧。

四、Result Api 的封裝

我們需要做的是:

第一步我們把回撥封裝到launch方法中,並簡化建立的物件方式

第二步我們嘗試自動註冊的功能

4.1 封裝簡化建立方式

首先第一步,我們對 Launcher 物件做一個封裝, 把 ActivityResultCallback 回撥方法在 launch 方法中呼叫。

/**
 * 對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) {
                //呼叫自己的擴充套件方法-陣列轉Bundle
                putExtras(bundle.toBundle()!!)
            }
        }

        launch(intent, null, callback)

    }

}

注意這裡呼叫的是 ActivityResultContracts.StartActivityForResult() 並且泛型的兩個引數是 Intent 和 ActivityResult。

如果大家想獲取檔案,可以使用 GetContent() 泛型的引數就要變成 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("拿到返回資料:$data")
        }
    }

//或者使用我們自定義的簡潔方式

    fun resultTest() {

       safLauncher?.launch<Demo10Activity> { result ->
            val data = result.data?.getStringExtra("text")
            toast("拿到返回資料:$data")
        }

        safLauncher?.launch<Demo10Activity>(arrayOf("id" to "123", "name" to "zhangsan")) { result ->
            val data = result.data?.getStringExtra("text")
            toast("拿到返回資料:$data")
        }
    }

使用下來是不是簡單了很多了,我們只需要建立一個物件就可以了,拿到這個物件呼叫launch即可實現 startActivityForResult 的功能呢!

4.2 自動註冊/按需註冊

可以看到相比原始的用法,雖然我們現在的用法就簡單了很多,但是我們還是要在oncreate生命週期中建立 Launcher 物件,不然會報錯:

LifecycleOwners must call register before they are STARTED.

那我們有哪些方法處理這個問題?

1)基礎類別定義

我們都已經封裝成物件使用了,我們把建立的邏輯定義到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 物件,你這麼無腦建立出來消耗記憶體。

有辦法,按需載入!

2).懶載入

懶載入可以吧,我需要的時候就建立。

abstract class AbsActivity() : AppCompatActivity(){

    val safLauncher by lazy { GetSAFLauncher(this) }

    ...
}

額,等等,這樣的懶載入貌似是不行的,這在用的時候才初始化,一樣會報錯:

LifecycleOwners must call register before they are STARTED.

我們只能在頁面建立的時候就要明確,這個頁面是否需要這個 Launcher 物件,如果要就要在onCreate中建立物件,如果確定不要 Launcher 物件,那麼就不必建立物件。

那我們就這麼做:

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就行了。預設是不建立這個物件的。

3).Kotlin委託

我們可以使用Kotlin的委託方式,把初始化的程式碼和 Launcher 的物件獲取用介面封裝,然後提供對應的實現類,不就可以完成按需新增 Launcher 的效果了嗎?

我們定義一個介面,由於邏輯都封裝在了別處,這裡就儘量不改動之前的程式碼,只是定義初始化和提供物件兩種方法。

/**
 * 定義是否需要SAFLauncher
 */
interface ISAFLauncher {

    fun <T : ActivityResultCaller> T.initLauncher()

    fun getLauncher(): GetSAFLauncher?

}

接著定義這個實現類

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("拿到返回資料:$data")
        }
    }

}

效果都是一樣的:

這樣通過委託的方式,我們就能自己管理初始化,自己隨時獲取到物件呼叫launch方法。

如果你當前的Activity不需要 startActivityForResult 這種功能,那麼你不實現這個介面即可,如果想要 startActivityForResult 的功能,就實現介面委託實現,從而實現按需載入的邏輯。

我們再回顧一下 Result Api 需要封裝的兩個痛點與優化步驟:

  • 第一步我們把回撥封裝到launch方法中,並簡化建立的物件方式
  • 第二步我們嘗試自動註冊的功能

同時我們還對一些步驟做了更多的可能性分析,對主動註冊的方式我們有三種方式,(當然其實還有更多別的方式來實現,我只寫了我認為比較簡單方便的幾種方式)。

到此對 Result Api的封裝就此結束。

總結

總的來說 Result Api 的封裝其實也不難,使用起來也是很簡單了。如果大家是Kotlin專案我推薦使用委託的方式,如果是Java語言開發的也可以用flag的方式實現按需載入的邏輯。

而不想使用 Result Api 那麼使用原始的 startActivityForResult 也能實現,那麼我推薦你使用 Ghost 框架,可以更加方便快速的實現返回的功能。

本文對於 Result Api 的封裝也只是限於 startActivityForResult 這一個場景,不過我們這種方式是很方便擴充套件的,如果大家想使用Result Api的方式來操作許可權,檔案等,都可以在 BaseResultLauncher 基礎上進行擴充套件。

到此這篇關於Android startActivityForResult的呼叫與封裝詳解的文章就介紹到這了,更多相關Android startActivityForResult內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com