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
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 Fragment
s!. 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.