Suppose we have a common scenario like this:

  • There are two activities, and the first Activity displays a text
  • Click the Edit button to start the second Activity and pass this text as a parameter to the second Activity
  • Edit this string in the second Activity
  • After editing, click Save to return the result to the first Activity
  • The first Activity shows the modified string

The diagram below:

This is a very simple and common scenario. We usually pass the parameters in startActivityForResult and receive the edited result in onActivityResult. The code is very simple as follows:


// The first Activity starts the edit Activity
btnEditByTradition.setOnClickListener {
    val content = tvContent.text.toString().trim()
    val intent = Intent(this, EditActivity::class.java).apply {
        putExtra(EditActivity.ARG_TAG_CONTENT, content)
    }
    startActivityForResult(intent, REQUEST_CODE_EDIT)
}
 //EditActivity returns the edited results
 btnSave.setOnClickListener {
    val newContent = etContent.text.toString().trim()
    setResult(RESULT_OK, Intent().apply {
        putExtra(RESULT_TAG_NEW_CONTENT, newContent)
    })
    finish()
}
// The edited result is accepted in the first Activity and displayed
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    when (requestCode) {
        REQUEST_CODE_EDIT -> {
            if(resultCode == RESULT_OK && data ! =null) {
                val newContent = data.getStringExtra(EditActivity.RESULT_TAG_NEW_CONTENT)
                tvContent.text = newContent
            }
        }
        else -> super.onActivityResult(requestCode, resultCode, data)
    }
}

Copy the code

So what are the downsides to this approach?

  1. Code scattered, poor readability
  2. The wrapper is not complete, the caller needs to go to EditActivity to know what parameters to pass, what type, what key
  3. The caller needs to know how the EditActivity returns the parameter type and what the key is in order to parse it correctly
  4. Poor constraint, constant definitions (REQUEST_CODE,PARAM_KEY, etc.), if the project management is not rigorous, repeated definition, resulting in late reconstruction and maintenance is difficult

Is there a way to solve the above disadvantages? What we expect is:

  1. An Activity that provides some external functionality should be wrapped so that the caller can call it as if it were a normal method in a single line of code
  2. The argument list of the method is the parameters that need to be passed to call the service (number of arguments, type of arguments, if necessary).
  3. The return parameter of the method is the return result of the service
  4. An Activity that provides a service, like a component, can provide external functions in the form of a method

This is done with a Kotlin coroutine and an invisible Fragment.


btnEditByCoroutine.setOnClickListener {
    GlobalScope.launch {
        val content = tvContent.text.toString().trim()

        Call the editContent method of the EditActivity
        // Content indicates the content to be edited
        // editContent is the edited result
        val newContent = EditActivity.editContent(this@MainActivity, content)

        if(! newContent.isNullOrBlank()) { tvContent.text = newContent } } }Copy the code

From the above code, we can see that the call can be done with a single method, basically fulfilling the expectations mentioned above. So what happens inside the editContent method? Look at the following code:


/** * Edits the specified text *@paramContent Specifies the text to edit@returnNull indicates that the edited content is null, indicating that the user unedited */
@JvmStatic
suspend fun editContent(activity: FragmentActivity, content: String): String? =
    suspendCoroutine { continuation ->
        val editFragment = BaseSingleFragment().apply {
            intentGenerator = {
                Intent(it, EditActivity::class.java).apply {
                    putExtra(ARG_TAG_CONTENT, content)
                }
            }
            resultParser = { resultCode, data ->
                if(resultCode == RESULT_OK && data ! =null) {
                    val result = data.getStringExtra(RESULT_TAG_NEW_CONTENT)
                    continuation.resume(result)
                } else {
                    continuation.resume(null)
                }
                removeFromActivity(activity.supportFragmentManager)
            }
        }
        editFragment.addToActivity(activity.supportFragmentManager)
    }

Copy the code

I need a “BaseSingleFragment” to do this because I can’t violate ActivityManagerService rules, StartActivityForResult and onActivityResult are still required, so we encapsulate this process with an invisible Fragment as follows:


class BaseSingleFragment : Fragment(a){


  /** * Generates an Intent that starts the corresponding Activity. Since the Intent specifies the Activity to start, how to start it, and how to pass parameters, the user must implement the lambda or throw an exception
  var intentGenerator: ((context: Context) -> Intent) = {
    throw RuntimeException("you should provide a intent here to start activity")}/** * Parses the result returned by the target Activity, which the implementer parses, and returns the message that the consumer must implement the lambda or throw an exception */
  var resultParser: (resultCode: Int, data: Intent?) -> Unit = { resultCode, data ->
    throw RuntimeException("you should parse result data yourself")
  }

  companion object {
    const val REQUEST_CODE_GET_RESULT = 100
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val context = requireContext()
    startActivityForResult(intentGenerator.invoke(context), REQUEST_CODE_GET_RESULT)
  }

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CODE_GET_RESULT) {
      resultParser.invoke(resultCode, data)
    } else {
      super.onActivityResult(requestCode, resultCode, data)
    }
  }


  /** * add current fragment to FragmentManager */
  fun addToActivity(fragmentManager: FragmentManager) {
    fragmentManager.beginTransaction().add(this.this::class.simpleName)
      .commitAllowingStateLoss()
  }

  /** * remove current fragment from FragmentManager */
  fun removeFromActivity(fragmentManager: FragmentManager) {
    fragmentManager.beginTransaction().remove(thisJava does not support coroutine. The reality is that many projects integrate Kotlin in mid-stream and have a lot of legacy Java code. Do we need to provide a Java implementation? The answer is No. Java code can also call The suspend method as follows:  btnEditByCoroutine.setOnClickListener((view) -> { String content = tvContent.getText().toString().trim(); EditActivity.editContent(MainActivityJava.this, content, new Continuation<String>() {
        @NotNull
        @Override
        public CoroutineContext getContext(a) {
            return EmptyCoroutineContext.INSTANCE;
        }

        @Override
        public void resumeWith(@NotNull Object o) {
            String newContent = (String) o;
            if(! TextUtils.isEmpty(content)) { tvContent.setText(newContent); }}}); });Copy the code

Although the result is received in the resumeWith method via a callback, it is still much better than the startActivityForResult method.

Perfect!!!

This implementation is inspired by RxPermission’s implementation of the permission application process, so thank you to RxPermission. In addition, Glide 3.X version Glide can start, pause, and cancel image loading tasks by adding a hidden Fragment to the FragmentManager. The code for this demo is in

CourtineTest GitHub ,

Still not perfect, if you have a better plan, or suggestions, please rely on me, might as well discuss next ha.