Activity / Hilt / Jetpack

Activity Result Contract – Outside The Activity

In the previous article, we looked at how the new Activity Result Contract pattern. It can keep our code much easier to understand. It can also reduce boilerplate which is always a good thing. This is fine when we need to, for example, request permissions in our Activity. But often we want to avoind putting too much logic in to our Activity. In this post we’ll look at various techniques for achieving that.

Background

Original image by janjf93
from Pixabay

Although I’ll be focusing on runtime permissions in this article, please remember that this technique can be used wherever startActivityForReult() would normally be used.

Runtime permissions fall into two distinct categories. There are those that are core to the operation of an app, and there are those which are secondary. For example, a camera app will require the CAMERA permission. It will be unable to perform its primary function without it. The app may have an optional feature to store location information with any photos taken. The location cannot be included if the necessary location permission is not granted. However, it does not prevent the photo from being taken. Therefore, for this app, the CAMERA permission is a strict requirement, but the location permission is secondary to the main function of the app.

We should request the required permissions up-front before the user enters the app. But we can embed the secondary permissions deeper within the app. Then we can request them at a point where they make sense to the user. In the example, we add a note to the image display on the screen after a photo has been taken. This note can inform the user that it is possible to add location information to photos. Then lead them through the location permission flow.

Fragments

Moving our runtime permissions logic to the Fragment has always added complexity. We need to either have the Fragment code calling methods of its parent Activity, or we introduce interfaces to handle that.

Starting in AndroidX Fragment 1.3.0-alpha02 we have ActivityResult support directly within our Fragments!. Thh API is almost identical to that in Activity:

class GrantedPermissionsFragment : Fragment(R.layout.fragment_granted_permissions) {

    private val getPermission = registerForActivityResult(RequestPermission()) { granted ->
        if (granted) {
            binding.secondaryPermission.setText(R.string.permission_granted)
        } else {
            binding.secondaryPermission.setText(R.string.permission_denied)
        }
    }

    lateinit var binding: FragmentGrantedPermissionsBinding

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding = FragmentGrantedPermissionsBinding.bind(view)

        binding.requestSecondary.setOnClickListener {
            getPermission.launch(Manifest.permission.READ_CONTACTS)
        }
    }
}

Internally this is marshalling the behaviour through the parent Activity but it certainly keeps our Fragment code much cleaner.

External Logic

Let’s not stop there! There may be cases where we may want to request permissions from other classes. Yet again, the Activity Result Contract makes this clean and easy:

class MyLogic(registry: ActivityResultRegistry) {

    private val enabled = MutableLiveData(false)

    private val getPermission = registry.register(REGISTRY_KEY, RequestPermission()) { granted ->
        enabled.value = granted
    }

    fun doSomething(): LiveData {
        getPermission.launch(Manifest.permission.READ_CALENDAR)
        return enabled
    }

    companion object {
        private const val REGISTRY_KEY = "Read Calendar Permission"
    }
}

This is a very simple example as it does nothing other than request the permission. However, it does show how we can easily separate our logic. In this case, when doSomething() is called, the required permission is requested, and the value of the LiveData<Boolean> will change to indicate the requested permission state.

What differs from the previous examples is that we cannot call registerForActivityResult() because it is not available in this context. However, the constructor takes an ActivityResultRegistry instance. This provides us with all we need. calling register() on the ActivityResultRegistry instance is the equivalent to calling registerForActivityResult(). The only difference is that we must provide a unique key that identifies the contract being registered. This associates the contract with the consumer. The rest works much the same as before.

It’s worth mentioning that the implementation of registerForActivityResult() for Fragment actually uses ActivityResultRegistry internally, although it is hidden from us.

We can then use this from our Fragment:

class GrantedPermissionsFragment : Fragment(R.layout.fragment_granted_permissions) {

    lateinit var myLogic: MyLogic

    private val getPermission = registerForActivityResult(RequestPermission()) { granted ->
        if (granted) {
            binding.secondaryPermission.setText(R.string.permission_granted)
        } else {
            binding.secondaryPermission.setText(R.string.permission_denied)
        }
    }

    lateinit var binding: FragmentGrantedPermissionsBinding

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding = FragmentGrantedPermissionsBinding.bind(view)

        binding.requestSecondary.setOnClickListener {
            getPermission.launch(Manifest.permission.READ_CONTACTS)
        }
        
        myLogic = MyLogic(requireActivity().activityResultRegistry)

        binding.requestExternal.setOnClickListener {
            myLogic.doSomething().observe(viewLifecycleOwner) { granted ->
                if (granted) {
                    binding.externalPermission.setText(R.string.permission_granted)
                } else {
                    binding.externalPermission.setText(R.string.permission_denied)
                }
            }
        }
    }
}

That’s pretty straightforward, but we have introduced knowledge of the parent Activity when we obtain the ActivityResultRegistry from it (line 24).

Hilt

We can easily remove this parent Activity reference from our Fragment if we’re using dependency Injection such as Hilt or Dagger (upon which Hilt sits). First, we need to create a module that provides the ActivityResultRegistry from the current Activity:

@Module
@InstallIn(ActivityComponent::class)
class ActivityModule {

    @Provides
    fun provideActivityResultRegistry(@ActivityContext activity: Context) =
        (activity as? AppCompatActivity)?.activityResultRegistry
            ?: throw IllegalArgumentException("You must use AppCompatActivity")
}

Next we make a small change to MyLogic to use an @Inject constructor:

class MyLogic @Inject constructor(registry: ActivityResultRegistry) {

    private val enabled = MutableLiveData(false)

    private val getPermission = registry.register(REGISTRY_KEY, RequestPermission()) { granted ->
        enabled.value = granted
    }

    fun doSomething(): LiveData {
        getPermission.launch(Manifest.permission.READ_CALENDAR)
        return enabled
    }

    companion object {
        private const val REGISTRY_KEY = "Read Calendar Permission"
    }
}

MyLogic can now be constructed and injected in to our Fragment:

@AndroidEntryPoint
class GrantedPermissionsFragment : Fragment(R.layout.fragment_granted_permissions) {

    @Inject
    lateinit var myLogic: MyLogic

    private val getPermission = registerForActivityResult(RequestPermission()) { granted ->
        if (granted) {
            binding.secondaryPermission.setText(R.string.permission_granted)
        } else {
            binding.secondaryPermission.setText(R.string.permission_denied)
        }
    }

    lateinit var binding: FragmentGrantedPermissionsBinding

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding = FragmentGrantedPermissionsBinding.bind(view)

        binding.requestSecondary.setOnClickListener {
            getPermission.launch(Manifest.permission.READ_CONTACTS)
        }

        binding.requestExternal.setOnClickListener {
            myLogic.doSomething().observe(viewLifecycleOwner) { granted ->
                if (granted) {
                    binding.externalPermission.setText(R.string.permission_granted)
                } else {
                    binding.externalPermission.setText(R.string.permission_denied)
                }
            }
        }
    }
}

We no longer need to reference the Activity from our Fragment code.

Conclusion

These are very well designed APIs that integrate almost seamlessly within Activity and Fragment. But are also very easy to use even outside of those contexts. This external use becomes even cleaner when used with Hilt.

The source code for this article is available here.

© 2021, Mark Allison. All rights reserved.

Copyright © 2021 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.