在官方Android文档的「应用权限最佳实践」中提到:为了增加权限请求被接受的可能性,只有在需要特定功能时才提示用户。例如,只有在用户点击麦克风按钮时才提示麦克风访问权限。用户更有可能允许他们期望的权限。
在Android 12之前,我们无法完全控制何时请求通知权限。系统将在创建第一个通知渠道时显示对话框。在单个活动应用程序中,这经常导致应用程序启动后立即显示提示。
从Android 13开始,可以决定何时显示提示。可以在特定屏幕进入、按钮点击或任何地方启动它。无论哪种方式都是有意义的。
为了演示,假设我们正在开发一个音乐流媒体应用程序。它是一个多屏幕、单活动架构的MVVM应用程序,并且还应该具备单元测试。
用户可以收到关于他关注的艺术家发布的新歌的通知。当用户将艺术家添加到收藏夹时,我们希望请求通知权限。正如Android文档建议的那样,在功能的上下文中执行此操作。
但是,实际实现起来有多简单呢?让我们看看官方Android文档对于请求运行时权限的说法。
-
1. 在您的活动或片段的初始化逻辑中,将ActivityResultCallback的实现传递给registerForActivityResult()方法的调用。ActivityResultCallback定义了应用程序如何处理用户对权限请求的响应。保留对registerForActivityResult()的返回值的引用,该引用是ActivityResultLauncher类型。
-
2. 要在必要时显示系统权限对话框,请调用之前保存的ActivityResultLauncher实例上的launch()方法。
调用launch()方法后,系统权限对话框将显示。当用户做出选择时,系统会异步调用您在前一步中定义的ActivityResultCallback的实现。
尝试一下。
class MainActivity : ComponentActivity() {
private var requestLauncher: ActivityResultLauncher<String>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { hasUserAccepted ->
// 处理用户响应
}
// 准备启动,但是何时启动呢?
requestLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
考虑到我们使用的是单Activity架构和MVVM,我们可以预期每个屏幕都有对应的片段/可组合项以及表示它们的视图模型。因此,在MainActivity中没有对特定屏幕的视图进行引用,我们无法添加点击侦听器并调用requestLauncher.launch()。
那么,让我们尝试使用片段。
class ArtistPageFragment : Fragment() {
private val artistPageViewModel: ViewModel by viewModel()
private var requestLauncher: ActivityResultLauncher<String>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { hasUserAllowed ->
viewModel.onUserPermissionResponse(hasUserAllowed)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
followArtistButton.setOnClickListener { artist ->
viewModel.followArtist(artist)
requestLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}
将点击侦听器附加到我们假想的关注按钮。每当用户点击它时,可以发起权限请求。
这看起来很好。对于很多情况来说,这已经足够了。
但是,再进一步。
假设这是一个代表音乐流媒体应用程序中艺术家页面的视图模型的示例。这只是一个例子。
class ArtistsPageViewModel(
private val artistsService: ArtistsService,
private val followedDatabase: FollowedDatabase,
private val playerController: PlayerController,
private val userPreferences: UserPreferences
) : ViewModel() {
private val _artistData: MutableStateFlow<Artist?> = MutableStateFlow(null)
val artistData: StateFlow<Artist> = _artistData
fun fetchArtist(id: Int) {
viewModelScope.launch {
_artistData.value = artistsService.fetch(id)
}
}
fun addArtistToFavourites(artist: Artist) {
followedDatabase.add(artist)
}
fun playArtistFeaturedSong(featuredSongId: Int) {
playerController.play(featuredSongId)
}
fun onUserPermissionResponse(response: Boolean) {
userPreferences.setNotificationPermissionResponse(response)
}
}
关于这个类,有什么可以说的呢?
-
• 它的功能由不同的对象组成,这些对象提供对它们的功能的访问(ArtistsService、FollowedDatabase、PlayerController、UserPreferences)。
-
• 由于我们可以模拟组合对象的行为,这个类的功能可以进行单元测试。
我希望将整个屏幕功能封装在一个模块化的视图模型中,利用组合的简单性和灵活性。为什么请求权限应该是例外,并且需要额外的设置呢?
在上面的示例中,可以看到,我们在片段中注册了一个请求权限的ActivityResultLauncher。然后,在点击事件中,启动了权限请求。这个方法对于单个屏幕来说已经足够简单了。
但是,如果我们希望将权限请求的逻辑与特定屏幕的视图模型解耦,该怎么办呢?
一种方法是使用依赖注入和组合。我们可以将权限请求的逻辑封装在一个单独的类中,并将其作为依赖注入到相应的视图模型中。这样,视图模型不需要知道关于权限请求的任何细节,只需调用适当的方法即可。
class PermissionsManager @Inject constructor(
private val activity: Activity,
private val permission: String
) {
private var requestLauncher: ActivityResultLauncher<String>? = null
fun registerPermissionCallback(callback: (Boolean) -> Unit) {
requestLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { hasUserAccepted ->
callback(hasUserAccepted)
}
}
fun requestPermission() {
requestLauncher?.launch(permission)
}
}
现在,我们可以在视图模型中使用PermissionsManager来处理权限请求。
class ArtistPageViewModel @Inject constructor(
private val permissionsManager: PermissionsManager
) : ViewModel() {
// ...
fun followArtist(artist: Artist) {
// 添加艺术家到收藏夹的逻辑
permissionsManager.registerPermissionCallback { hasUserAllowed ->
// 处理权限请求的结果
if (hasUserAllowed) {
// 用户授予了权限
} else {
// 用户拒绝了权限
}
}
permissionsManager.requestPermission()
}
}
现在,我们将权限请求的逻辑封装在PermissionsManager类中,视图模型只需要关注艺术家的关注逻辑。当权限请求的结果返回时,PermissionsManager将调用回调方法,视图模型可以根据用户的响应进行相应的处理。
总结
Android 13为我们提供了更灵活的方式来请求运行时权限。通过使用依赖注入和组合,我们可以将权限请求的逻辑封装在一个单独的类中,使代码更加模块化和可测试。在这篇文章中,我们首先了解了官方Android文档中有关请求运行时权限的最佳实践。然后,我们看了一些示例代码,展示了如何在特定屏幕中请求权限。
然而,我们意识到将权限请求逻辑与特定屏幕的视图模型解耦是更好的做法。为此,引入了依赖注入和组合的概念。
创建了一个名为PermissionsManager的类,它负责处理权限请求。该类通过依赖注入获得Activity和权限字符串,并使用ActivityResultContracts.RequestPermission()注册了一个ActivityResultLauncher。然后,我们在视图模型中使用PermissionsManager,并调用registerPermissionCallback()方法注册权限请求的回调函数。最后,调用requestPermission()方法触发权限请求。
通过将权限请求逻辑从视图模型中分离出来,使代码更加模块化和可测试。视图模型只需关注与艺术家相关的逻辑,而不需要了解权限请求的细节。
这种灵活的方式使我们能够在应用程序的任何地方请求权限,而不仅仅局限于特定屏幕。这为我们带来了更大的自由度和可扩展性。
总结一下,通过使用依赖注入和组合的方法,我们可以在Android 13上以一种灵活的方式请求运行时权限。这种方式使我们的代码更具模块化、可测试和可扩展的特性,提高了应用程序的质量和开发效率。