今天延續Day16 MVVM專案-3 RecyclerView
有些權限在android6.0以上需要動態註冊
今天會寫個獲取聯絡人的功能來當範例
首先先去模擬器新增一個聯絡人
然後開啟專案
AndroidManifest.xml
<application
...
>
<uses-permission android:name="android.permission.READ_CONTACTS" />
</application>
先宣告權限
新增viewModel
S04ViewModel.kt
class S04ViewModel : ViewModel() {
val nameAndPhone = MutableLiveData<Pair<String, String>>()
val contact: LiveData<String> = Transformations.map(nameAndPhone) { (name, phone) ->
"Name : $name\nPhone : $phone"
}
val pickContactAction = MutableLiveData<Unit>()
fun pickContact() {
pickContactAction.value = Unit
}
}
註冊權限的class
UserPermission.kt
class UserPermission(private val activity: Activity, vararg permissions: String) {
val perms: Array<out String> by lazy { permissions }
lateinit var isGrantedCallback: () -> Unit
lateinit var notGrantedCallback: () -> Unit
/**
* 1. 確認權限是否註冊
* 2. 如果註冊 呼叫`isGranted`, 或註冊權限
* 3. 根據回傳結果呼叫 `isGranted` 或 `notGranted`
*/
fun checkOrRequest(isGranted: () -> Unit, notGranted: () -> Unit) {
val notGrantedPermissions = perms.filter {
ContextCompat.checkSelfPermission(
activity,
it
) != PackageManager.PERMISSION_GRANTED
}
.toTypedArray()
val permissionIsAllGranted = notGrantedPermissions.isEmpty()
if (permissionIsAllGranted) {
isGranted()
} else {
isGrantedCallback = isGranted
notGrantedCallback = notGranted
val requestCode =
Math.abs((activity.hashCode() + isGranted.hashCode()).toShort().toInt())
activity.permissions.put(requestCode, this)
ActivityCompat.requestPermissions(activity, notGrantedPermissions, requestCode)
}
}
override fun toString(): String = "${javaClass.simpleName} ${Arrays.toString(perms)}"
}
private val Activity.permissions by lazy { SparseArray<UserPermission>() }
fun Activity.permissionOf(vararg permissions: String): UserPermission =
UserPermission(this, *permissions)
/**
* this has to be called in Activity.onRequestPermissionsResult
*/
fun Activity.requestPermissionResult(requestCode: Int, grantResults: IntArray) =
permissions[requestCode]?.let {
permissions.remove(requestCode)
val isGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
if (isGranted) it.isGrantedCallback() else it.notGrantedCallback()
}
首頁
S04Activity.kt
class S04Activity : AppCompatActivity() {
val permissRequestCode = 100
private val viewModel: S04ViewModel by lazy {
ViewModelProviders.of(this).get(S04ViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivityS04Binding>(this, R.layout.activity_s04).let {
it.setLifecycleOwner(this)
it.viewModel = viewModel
}
viewModel.pickContactAction.observe(this, Observer {
permissionOf(Manifest.permission.READ_CONTACTS).checkOrRequest({
val intent =
Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
startActivityForResult(intent, permissRequestCode)
}, {
Toast.makeText(this, "notGranted", Toast.LENGTH_SHORT).show()
})
})
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
requestPermissionResult(requestCode, grantResults)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == permissRequestCode && resultCode == Activity.RESULT_OK && data?.data != null) {
val cursor = contentResolver.query(data.data!!, null, null, null, null)
val nameAndPhone = cursor?.use {
it.moveToFirst()
val name =
it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME))
val phone =
it.getString(it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DATA))
name to phone
} ?: "" to ""
viewModel.nameAndPhone.value = nameAndPhone
}
}
}
xml
activity_s04.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.ithome11.jetpackmvvmdemo.main.s04.S04ViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/bt_pick_contact"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:onClick="@{_ -> viewModel.pickContact()}"
android:text="pick contact"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_contact"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{viewModel.contact}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bt_pick_contact" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
完成後的畫面
最後新增單元測試
class S04ViewModelTest {
@Rule
@JvmField
val rule = InstantTaskExecutorRule()
@Test
fun pickContact() {
val viewModel = S04ViewModel()
val lifecycle = LifecycleRegistry(mockk()).apply {
handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
// given
val givenName = "abc"
val givenPhone = "111"
viewModel.contact.observe({ lifecycle }) {}
viewModel.pickContactAction.observe({ lifecycle }) {
viewModel.nameAndPhone.value = givenName to givenPhone
}
// when
viewModel.pickContact()
// then
Assert.assertEquals("Name : $givenName\nPhone : $givenPhone", viewModel.contact.value)
}
}
solution
https://github.com/mars1120/jetpackMvvmDemo/tree/mvvm-04-RequestPermission