DEV Community

Nikola Despotoski
Nikola Despotoski

Posted on

Consume Activity result directly in the ViewModel

Most common implementation of Delegation Pattern is when the responsibility of the class is delegated to a real object of a concrete implementation of the same interface.

class HomeAcrobat implements Acrobat {
  private final Acrobat mProAcrobat = new CircusAcrobat();
  @Override
  public void doAmazingStuff(){
     mProAcrobat.doAmazingStuff();
  } 
}

Kotlin gives amazing and neat way to implement Delegation pattern using the keyword by. This gives impression of having multiple inheritance, it is still a polymorphisms in a sense.

In activity-ktx:1.3.+, requesting and receiving results where taken to a new level with introduction of ActivityResultContract<I, O> and ActivityResultCallback. ActivityResultContract<I, O> is an interface that performs how and who will get the request for result, and ActivityResultCallback is how the consumer will consume the result.

But Google left us with ActivityResultContract abstract class which is the only obstacle we have to jump.

Consider we are advocates of separation of concerns and we want to take take the parsing of the result in another class.

@FragmentScope
class ChooseFilterContractor @Inject constructor(private val context: Context) : ActivityResultContractor<Unit, String> {
    override fun createIntent(input: Unit): Intent {
        return Intent(context, ChooseFilterActivity::class.java)
    }

    override fun parseResult(resultCode: Int, intent: Intent?): String {
        return if (resultCode == Activity.RESULT_OK) {
            intent?.getStringExtra(KEY_FILTER)?: error("Dispatched RESULT_OK, but  no payload)
        } else {
            ""
        }
    }
}

You may notice the interface ActivityResultContractor<I, O>, this is extracted interface from ActivityResultContract.

With a minor tweak to the prepareCall() function, we can wrap our contractor to work as library provided ActivityResultContract.

fun <I, O> prepareCall(
        activityResultContractor: ActivityResultContractor<I, O>,
        activityResultCallback: ActivityResultCallback<O>
    ): ActivityResultLauncher<I> {

        return prepareCall(object : ActivityResultContract<I, O>() {
            override fun createIntent(input: I): Intent = activityResultContractor.createIntent(input)

            override fun parseResult(resultCode: Int, intent: Intent?): O =
                activityResultContractor.parseResult(resultCode, intent)
        }, activityResultCallback)
    }

This function will be called from the Fragment or Activity, since they are only able to launch another Activity.

Our goal is to make the ViewModel be able to parse and receive the result. You may notice that now the ProductViewModel also acts like a contractor but the parsing of the result is delegated to the activityResultContractor using this syntax ActivityResultContractor<Unit, String> **by** activityResultContractor.

class ProductsViewModel @Inject constructor(
    private val activityResultContractor: ActivityResultContractor<Nothing, String>,
    private val coroutineProvider: CoroutineProvider,
    private val productInteractor: ProductInteractor
) : ViewModel(), ActivityResultCallback<String>,
 ActivityResultContractor<Unit, String> by activityResultContractor{

     override fun onActivityResult(result: String?) {
        // do something with the result
    }

}

Final call to the tweaked prepareCall() would be:

      chooseProductFilter.setOnClickListener {
          prepareCall(productViewModel, productViewModel).launch(Unit)
      }

Another example of calling launch, when the input type is not Unit:

      textLiveData.observe(viewLifecycle){
          prepareCall(productViewModel, productViewModel).launch(it)
      }

Top comments (0)