I created a two screen verification app where in the first screen you enter the Phone Number and in the second screen you need to type the OTP. Android has broadcast service to automatically detect the otp but this is isn't working. I took reference from here and here. My code is:
#Composable
fun VerifyScreen(
navHostController: NavHostController,
viewModel: LoginViewModel = hiltViewModel(),
number: String,
onClick: (otp: String) -> Unit,
) {
val otp = viewModel.otp.value
SmsRetrieverUserConsentBroadcast { _, code -> viewModel.onEvent(UserDataEvent.EnteredOTP(code)) }
MindYugTheme {
Scaffold(
topBar = {
IconButton(
onClick = { navHostController.navigateUp() },
) {
Icon(
imageVector = Icons.Outlined.ArrowBackIos,
contentDescription = "back",
tint = MaterialTheme.colors.secondary
)
}
},
) {
val phoneNumber = buildAnnotatedString {
withStyle(style = SpanStyle(color = Color(0xFFFFFFFF))) {
append("$number ")
}
pushStringAnnotation(
tag = "resend",// provide tag which will then be provided when you click the text
annotation = "resend"
)
withStyle(style = SpanStyle(color = Color(0xFFB5B4ED))) {
append("RESEND")
}
pop()
}
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.align(Alignment.Start),
text = "Verify",
style = Typography.h4
)
Spacer(modifier = Modifier.height(16.dp))
OTPTextFields(
length = 6
) { getOpt ->
otp.text = getOpt
}
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Please enter the OTP sent to:")
// Text(text = number)
ClickableText(text = phoneNumber, onClick = { offset ->
phoneNumber.getStringAnnotations(
tag = "resend",
start = offset,
end = offset
).firstOrNull()?.let {
Log.d("tag", "clicked")
}
})
Spacer(modifier = Modifier.height(24.dp))
GradientButton(onClick = {
navHostController.navigate(Screen.EnterNameScreen.route)
onClick(otp.text)
}) {
// otpVerification(otp, context)
Text(text = "Next")
}
}
}
}
}
#Composable
fun SmsRetrieverUserConsentBroadcast(
smsCodeLength: Int = 6,
onSmsReceived: (message: String, code: String) -> Unit,
) {
val context = LocalContext.current
var shouldRegisterReceiver by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
SmsRetriever.getClient(context)
.startSmsUserConsent(null)
.addOnSuccessListener {
shouldRegisterReceiver = true
}
}
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it?.resultCode == Activity.RESULT_OK && it.data != null) {
val message: String? = it.data!!.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
message?.let {
val verificationCode = getVerificationCodeFromSms(message, smsCodeLength)
onSmsReceived(message, verificationCode)
}
shouldRegisterReceiver = false
} else {
}
}
if (shouldRegisterReceiver) {
SystemBroadcastReceiver(
systemAction = SmsRetriever.SMS_RETRIEVED_ACTION,
broadCastPermission = SmsRetriever.SEND_PERMISSION,
) { intent ->
if (intent != null && SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val smsRetrieverStatus = extras?.get(SmsRetriever.EXTRA_STATUS) as Status
when (smsRetrieverStatus.statusCode) {
CommonStatusCodes.SUCCESS -> {
// Get consent intent
val consentIntent =
extras.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
try {
// Start activity to show consent dialog to user, activity must be started in
// 5 minutes, otherwise you'll receive another TIMEOUT intent
launcher.launch(consentIntent)
} catch (e: ActivityNotFoundException) {
// Timber.e(e, "Activity Not found for SMS consent API")
}
}
CommonStatusCodes.TIMEOUT -> {
}
}
}
}
}
}
#Composable
fun SystemBroadcastReceiver(
systemAction: String,
broadCastPermission: String,
onSystemEvent: (intent: Intent?) -> Unit
) {
// Grab the current context in this part of the UI tree
val context = LocalContext.current
// Safely use the latest onSystemEvent lambda passed to the function
val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)
// If either context or systemAction changes, unregister and register again
DisposableEffect(context, systemAction) {
val intentFilter = IntentFilter(systemAction)
val broadcast = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
onSystemEvent(intent)
}
}
context.registerReceiver(broadcast, intentFilter)
// When the effect leaves the Composition, remove the callback
onDispose {
context.unregisterReceiver(broadcast)
}
}
DisposableEffect(context, broadCastPermission) {
val intentFilter = IntentFilter(broadCastPermission)
val broadcast = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
onSystemEvent(intent)
}
}
context.registerReceiver(broadcast, intentFilter)
// When the effect leaves the Composition, remove the callback
onDispose {
context.unregisterReceiver(broadcast)
}
}
}
internal fun getVerificationCodeFromSms(sms: String, smsCodeLength: Int): String =
sms.filter { it.isDigit() }
.substring(0 until smsCodeLength)
The error I'm getting is:
java.lang.RuntimeException: Error receiving broadcast Intent { act=com.google.android.gms.auth.api.phone.SMS_RETRIEVED flg=0x200010 pkg=com.mindyug.app (has extras) } in com.google.android.gms.internal.firebase-auth-api.zzvl#f0a6f7b
at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0$LoadedApk$ReceiverDispatcher$Args(LoadedApk.java:1581)
at android.app.-$$Lambda$LoadedApk$ReceiverDispatcher$Args$_BumDX2UKsnxLVrE6UJsJZkotuA.run(Unknown Source:2)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:233)
at android.app.ActivityThread.main(ActivityThread.java:8063)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'int java.lang.CharSequence.length()' on a null object reference
at java.util.regex.Matcher.reset(Matcher.java:256)
at java.util.regex.Matcher.<init>(Matcher.java:167)
at java.util.regex.Pattern.matcher(Pattern.java:1027)
at com.google.android.gms.internal.firebase-auth-api.zzvn.zzb(com.google.firebase:firebase-auth##21.0.1:1)
at com.google.android.gms.internal.firebase-auth-api.zzvl.onReceive(com.google.firebase:firebase-auth##21.0.1:8)
at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0$LoadedApk$ReceiverDispatcher$Args(LoadedApk.java:1566)
at android.app.-$$Lambda$LoadedApk$ReceiverDispatcher$Args$_BumDX2UKsnxLVrE6UJsJZkotuA.run(Unknown Source:2)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:233)
at android.app.ActivityThread.main(ActivityThread.java:8063)
Related
i am working on compose project. I have simple login page. After i click login button, loginState is set in viewmodel. The problem is when i set loginState after service call, my composable recomposed itself multiple times. Thus, navcontroller navigates multiple times. I don't understand the issue. Thanks for helping.
My composable :
#Composable
fun LoginScreen(
navController: NavController,
viewModel: LoginViewModel = hiltViewModel()
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly
) {
val email by viewModel.email
val password by viewModel.password
val enabled by viewModel.enabled
if (viewModel.loginState.value) {
navController.navigate(Screen.HomeScreen.route) {
popUpTo(Screen.LoginScreen.route) {
inclusive = true
}
}
}
LoginHeader()
LoginForm(
email = email,
password = password,
onEmailChange = { viewModel.onEmailChange(it) },
onPasswordChange = { viewModel.onPasswordChange(it) }
)
LoginFooter(
enabled,
onLoginClick = {
viewModel.login()
},
onRegisterClick = {
navController.navigate(Screen.RegisterScreen.route)
}
)
}
ViewModel Class:
#HiltViewModel
class LoginViewModel #Inject constructor(
private val loginRepository: LoginRepository,
) : BaseViewModel() {
val email = mutableStateOf(EMPTY)
val password = mutableStateOf(EMPTY)
val enabled = mutableStateOf(false)
val loginState = mutableStateOf(false)
fun onEmailChange(email: String) {
this.email.value = email
checkIfInputsValid()
}
fun onPasswordChange(password: String) {
this.password.value = password
checkIfInputsValid()
}
private fun checkIfInputsValid() {
enabled.value =
Validator.isEmailValid(email.value) && Validator.isPasswordValid(password.value)
}
fun login() = viewModelScope.launch {
val response = loginRepository.login(LoginRequest(email.value, password.value))
loginRepository.saveSession(response)
loginState.value = response.success ?: false
}
}
You should not cause side effects or change the state directly from the composable builder, because this will be performed on each recomposition.
Instead you can use side effects. In your case, LaunchedEffect can be used.
if (viewModel.loginState.value) {
LaunchedEffect(Unit) {
navController.navigate(Screen.HomeScreen.route) {
popUpTo(Screen.LoginScreen.route) {
inclusive = true
}
}
}
}
But I think that much better solution is not to listen for change of loginState, but to make login a suspend function, wait it to finish and then perform navigation. You can get a coroutine scope which will be bind to your composable with rememberCoroutineScope. It can look like this:
suspend fun login() : Boolean {
val response = loginRepository.login(LoginRequest(email.value, password.value))
loginRepository.saveSession(response)
return response.success ?: false
}
Also check out Google engineer thoughts about why you shouldn't pass NavController as a parameter in this answer (As per the Testing guide for Navigation Compose ...)
So your view after updates will look like:
#Composable
fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel(),
onLoggedIn: () -> Unit,
onRegister: () -> Unit,
) {
// ...
val scope = rememberCoroutineScope()
LoginFooter(
enabled,
onLoginClick = {
scope.launch {
if (viewModel.login()) {
onLoggedIn()
}
}
},
onRegisterClick = onRegister
)
// ...
}
And your navigation route:
composable(route = "login") {
LoginScreen(
onLoggedIn = {
navController.navigate(Screen.HomeScreen.route) {
popUpTo(Screen.LoginScreen.route) {
inclusive = true
}
}
},
onRegister = {
navController.navigate(Screen.RegisterScreen.route)
}
)
}
I have a composable function that looks something like this:
#Composable
fun listScreen(context: Context, owner: ViewModelStoreOwner) {
val repository = xRepository(getAppDatabase(context).xDao()
val listData by repository.readAllData.observeAsState(emptyList())
// repository.readAllData returns LiveData<List<xEntity>>
// listData is a List<xEntity>
LazyColumn(){
items(listData.size) {
Card {
Text(listData[it].name)
Text(listData[it].hoursLeft.toString())
Button(onClick = {updateInDatabase(owner, name = listData[it], hoursLeft = 12)}) {...}
}
}
}
}
fun updateInDatabase(owner: ViewModelStoreOwner, name: String, hoursLeft: Int) {
val xViewModel....
val newEntity = xEntity(name=name, hoursLeft = Int)
xViewModel.update(newEntity)
}
and as you propably can guess, the LazyColumn doesn't refresh after modification of entity, is there a way to update listData after every update of entity?
edit:
class xRepository(private val xDatabaseDao) {
val readAllData: LiveData<List<xEntity>> = xDatabaseDao.getallXinfo()
...
suspend fun updatePlant(x: xEntity) {
plantzDao.updateX(x)
}
}
interface xDatabaseDao {
#Query("SELECT * FROM xInfo ORDER BY id DESC")
fun getAllXInfo(): LiveData<List<xEntity>>
....
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun updateX(x: xEntity?)
}
modification of entity:
fun updatePlantInDatabase(owner: ViewModelStoreOwner, name: String, waterAtHour: Int, selectedDays: ArrayList<Int>) {
val xViewModel: xViewModel = ViewModelProvider(owner).get(xViewModel::class.java)
val new = xEntity(name = name, waterAtHour = waterAtHour, selectedDays = selectedDays)
xViewModel.updatePlant(new)
}
I use mutableStateOf to wrap fields that need to recomposed. Such as
class TestColumnEntity(
val id: String,
title: String = ""
){
var title: String by mutableStateOf(title)
}
View:
setContent {
val mData = mutableStateListOf(
TestColumnEntity("id_0").apply { title = "cnm"},
TestColumnEntity("id_1").apply { title = "cnm"},
TestColumnEntity("id_2").apply { title = "cnm"},
TestColumnEntity("id_3").apply { title = "cnm"},
TestColumnEntity("id_4").apply { title = "cnm"},
TestColumnEntity("id_5").apply { title = "cnm"},
)
Column {
Button(onClick = {
mData.add(TestColumnEntity("id_${Random.nextInt(100) + 6}").apply { title = "ccnm" })
}) {
Text(text = "add data")
}
Button(onClick = {
mData[1].title = "test_${Random.nextInt(100)}"
}) {
Text(text = "update data")
}
TestLazyColumn(data = mData, key = {index, item ->
item.id
}) {
Text(text = it.title)
}
}
}
It works in my testcase
If you want to update lazy column (say recompose in jetpack compose) so use side effects.
Put list getting function in side effect (Launch Effect or other side effects) when list is change side effect automatic recompose your function and show updated list.
So I have this composable which tries to read data from storage,
#Composable
private fun Screen() {
val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data.toString()
if(uri !== null) {
val file = File(uri)
val bytes = file.readBytes()
println(bytes)
}
}
Column() {
Button(onClick = {
val intent = Intent().setType("*/*").setAction(Intent.ACTION_OPEN_DOCUMENT)
launcher.launch(intent)
}) {
Text("Open file")
}
}
}
However, it gives me this error: content:/com.android.providers.downloads.documents/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2FIMG_CEFEFF486A8C-1.jpeg: open failed: ENOENT (No such file or directory). What am I doing wrong here? Please help.
Figured it out,
#Composable
private fun Screen() {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { result ->
val item = context.contentResolver.openInputStream(result)
val bytes = item?.readBytes()
println(bytes)
item?.close()
}
return Column {
Button(onClick = {
launcher.launch("*/*")
}) {
Text("Open file")
}
}
}
I have a problem for now in JetpackCompose.
The problem is, when I'm collecting the Data from a flow, the value is getting fetched from firebase like there is a listener and the data's changing everytime. But tthat's not that.
I don't know what is the real problem!
FirebaseSrcNav
suspend fun getName(uid: String): Flow<Resource.Success<Any?>> = flow {
val query = userCollection.document(uid)
val snapshot = query.get().await().get("username")
emit(Resource.success(snapshot))
}
NavRepository
suspend fun getName(uid: String) = firebase.getName(uid)
HomeViewModel
fun getName(uid: String): MutableStateFlow<Any?> {
val name = MutableStateFlow<Any?>(null)
viewModelScope.launch {
navRepository.getName(uid).collect { nameState ->
when (nameState) {
is Resource.Success -> {
name.value = nameState.data
//_posts.value = state.data
loading.value = false
}
is Resource.Failure<*> -> {
Log.e(nameState.throwable, nameState.throwable)
}
}
}
}
return name
}
The probleme is in HomeScreen I think, when I'm calling the collectasState().value.
HomeScreen
val state = rememberLazyListState()
LazyColumn(
state = state,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(post) { post ->
//val difference = homeViewModel.getDateTime(homeViewModel.getTimestamp())
val date = homeViewModel.getDateTime(post.timeStamp!!)
val name = homeViewModel.getName(post.postAuthor_id.toString()).collectAsState().value
QuestionCard(
name = name.toString(),
date = date!!,
image = "",
text = post.postText!!,
like = 0,
response = 0,
topic = post.topic!!
)
}
}
I can't post video but if you need an image, imagine a textField where the test is alternating between "null" and "MyName" every 0.005 second.
Check official documentation.
https://developer.android.com/kotlin/flow
Flow is asynchronous
On viewModel
private val _name = MutableStateFlow<String>("")
val name: StateFlow<String>
get() = _name
fun getName(uid: String) {
viewModelScope.launch {
//asyn call
navRepository.getName(uid).collect { nameState ->
when (nameState) {
is Resource.Success -> {
name.value = nameState.data
}
is Resource.Failure<*> -> {
//manager error
Log.e(nameState.throwable, nameState.throwable)
}
}
}
}
}
on your view
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
viewModel.name.collect { name -> handlename
}
}
}
I wrote utility functions to request/check permissions in Composables (using CompositionLocal).
data class PermissionHandlerValue(
val hasPermission: (String) -> Boolean,
val hasPermissions: (Array<out String>) -> Array<Boolean>,
val requestPermission: (String) -> Unit,
val requestPermissions: (Array<out String>) -> Unit
)
val LocalPermissionHandler = compositionLocalOf<PermissionHandlerValue> { error("No implementation provided!") }
#Composable
fun ProvidePermissionHandler(content: #Composable () -> Unit) {
CompositionLocalProvider(LocalPermissionHandler provides permissionHandlerImpl()) {
content()
}
}
#Composable
fun permissionHandlerImpl(): PermissionHandlerValue {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {}
val hasPermission: (String) -> Boolean = { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
val hasPermissions: (Array<out String>) -> Array<Boolean> = { it.map { permission -> hasPermission(permission) }.toTypedArray() }
val requestPermission: (String) -> Unit = { launcher.launch(arrayOf(it)) }
val requestPermissions: (Array<out String>) -> Unit = { launcher.launch(it) }
return PermissionHandlerValue(hasPermission, hasPermissions, requestPermission, requestPermissions)
}
#Composable
fun RequirePermission(permission: String, fallback: (#Composable () -> Unit)? = null, content: #Composable () -> Unit) {
val permissionHandler = LocalPermissionHandler.current
if (permissionHandler.hasPermission(permission))
content()
else if (fallback != null)
fallback()
}
It works fine, I can request and check permissions. The problem is that its not reactive, here's an example:
setContent {
ProvidePermissionHandler {
val permissionHandler = LocalPermissionHandler.current
RequirePermission(
permission = Manifest.permission.READ_CONTACTS,
fallback = {
Button(onClick = { permissionHandler.requestPermission(Manifest.permission.READ_CONTACTS)
}) {
Text("Request permission")
}
}
) {
ContactsList()
}
}
}
This composable(RequirePermission) will only render ContactsList if the Manifest.permission.READ_CONTACTS was granted, Otherwise the fallback component is rendered with a button that when clicked will request the permission.
After permissionHandler.requestPermission() is called and I grant the permission on the screen the fallback still shows, instead of the ContactsList (I have to re-open the app to show it).
Basically the condition in RequirePermission() is not checked again because there is no recomposition. How can I force RequirePermission() to recompose?