Reputation: 185
I am trying to work around the lack of desktop support from dev.icerock.moko:biometry-compose, I am not able to compile desktop version while this library is a part of commonMain. The idea is to push down the library to Android & Native (ios). I tried expect/actual and interface options. But I ran into this in both cases. "@composable invocations can only happen from the context of a @composable function". Any ideas?
interface BiometryAuthenticator {
suspend fun checkBiometryAuthentication(
requestTitle: String,
requestReason: String,
failureButtonText: String,
allowDeviceCredentials: Boolean
): Boolean
}
expect class BiometryAuthenticatorFactory() {
@Composable
fun createBiometryAuthenticator(): BiometryAuthenticator
}
// AndroidBiometryAuthenticatorFactory.kt (androidMain)
package domain
import androidx.compose.runtime.Composable
import dev.icerock.moko.biometry.compose.rememberBiometryAuthenticatorFactory
import dev.icerock.moko.resources.desc.Raw
import dev.icerock.moko.resources.desc.StringDesc
import dev.icerock.moko.biometry.BiometryAuthenticator as MokoBiometryAuthenticator
actual class BiometryAuthenticatorFactory {
@Composable
actual fun createBiometryAuthenticator(): BiometryAuthenticator {
val factory = rememberBiometryAuthenticatorFactory()
return MokoBiometryAuthenticatorWrapper(factory.createBiometryAuthenticator())
}
private class MokoBiometryAuthenticatorWrapper(
private val authenticator: MokoBiometryAuthenticator
) : BiometryAuthenticator {
override suspend fun checkBiometryAuthentication(
requestTitle: String,
requestReason: String,
failureButtonText: String,
allowDeviceCredentials: Boolean
): Boolean {
return authenticator.checkBiometryAuthentication(
requestTitle = StringDesc.Raw(requestTitle),
requestReason = StringDesc.Raw(requestReason),
failureButtonText = StringDesc.Raw(failureButtonText),
allowDeviceCredentials = allowDeviceCredentials
)
}
}
}
@Composable
fun LoginScreen() {
val viewModel = getViewModel(
key = "biometry-screen",
factory = viewModelFactory {
AuthViewModel(
biometryAuthenticatorFactory.createBiometryAuthenticator() <-- It breaks here
)
}
) {
Column() {
BindBiometryAuthenticatorEffect(viewModel.biometryAuthenticator) <-- and here....
}
Upvotes: 2
Views: 91
Reputation: 185
expect class BiometricAuth() {
@Composable
fun authenticate(onSuccess: () -> Unit, onError: (Exception) -> Unit)
}
Android:
actual class BiometricAuth actual constructor() {
@Composable
actual fun authenticate(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
val biometryAuthenticator = BiometryAuthenticator(appContext)
val viewModel: AndroidAuthViewModel = getViewModel(
key = "biometry-screen",
factory = viewModelFactory { AndroidAuthViewModel(biometryAuthenticator)
})
Column() {
BindBiometryAuthenticatorEffect(viewModel.biometryAuthenticator)
viewModel.tryToAuth(onSuccess, onError)
}
}
}
class MainActivity : FragmentActivity() { // ComponentActivity() {
companion object {
lateinit var appContext: Context
private set
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appContext = applicationContext
try {
setContent {
App()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
class AndroidAuthViewModel(val biometryAuthenticator: BiometryAuthenticator): ViewModel() {
fun tryToAuth(onSuccess: () -> Unit, onError: (Exception) -> Unit) = viewModelScope.launch {
val isSuccess = biometryAuthenticator.checkBiometryAuthentication(
requestTitle = "Biometry".desc(),
requestReason = "Invisible App needs your authentication".desc(),
failureButtonText = "Oops".desc(),
allowDeviceCredentials = true
)
if (isSuccess) {
onSuccess()
} else {
onError(RuntimeException("An error occurred"))
}
}
}
iOS
actual class BiometricAuth actual constructor() {
@Composable
actual fun authenticate(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
val biometryAuthenticator = BiometryAuthenticator()
val viewModel: IOSAuthViewModel = remember { IOSAuthViewModel(biometryAuthenticator) }
viewModel.tryToAuth(onSuccess, onError)
}
}
class IOSAuthViewModel(private val biometryAuthenticator: BiometryAuthenticator) {
private val mainScope = MainScope()
fun tryToAuth(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
mainScope.launch {
try {
val isSuccess = biometryAuthenticator.checkBiometryAuthentication(
requestTitle = "Biometry".desc(),
requestReason = "Invisible App needs your authentication".desc(),
failureButtonText = "Oops".desc(),
allowDeviceCredentials = true
)
if (isSuccess) {
onSuccess()
} else {
onError(RuntimeException("An error occurred"))
}
} catch (e: Exception) {
onError(e)
}
}
}
}
App
@Composable
@Preview
fun App() {
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
var errorContent by remember { mutableStateOf("") }
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
val auth = BiometricAuth()
auth.authenticate(
onSuccess = {
showContent = true
},
onError = { err ->
errorContent = err.message.toString()
}
)
Text("The value of error content is $errorContent")
AnimatedVisibility(showContent) {
val greeting = remember { Greeting().greet() }
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Image(painterResource(Res.drawable.compose_multiplatform), null)
Text("Compose: $greeting")
}
}
}
}
}
Upvotes: 0
Reputation: 88192
You can move createBiometryAuthenticator
outside of viewModelFactory
:
val biometryAuthenticator = biometryAuthenticatorFactory.createBiometryAuthenticator()
val viewModel = getViewModel(
key = "biometry-screen",
factory = viewModelFactory {
AuthViewModel(
biometryAuthenticator
)
}
) {
To make it more compose-like, I would suggest you rename the method to rememberBiometryAuthenticator
, and actually remember the value - otherwise the object will be created on each recomposition, and that is not much expected since the factory will not use the new value.
@Composable
actual fun rememberBiometryAuthenticator(): BiometryAuthenticator {
val factory = rememberBiometryAuthenticatorFactory()
return remember { MokoBiometryAuthenticatorWrapper(factory.createBiometryAuthenticator()) }
}
Upvotes: 1