I'm doing an application with Jetpack Compose and Kotlin. It is an app to locate an android device. I need to implement run time permissions to follow the jetpack filosofy.
I have a menu page where there is a switch that when is activate saves the location of the device but just activate the switch it is necessary to request permissions "fine_location", "coarse_location" and "back_groundlocation". This is my menu.kt code:
LazyColumn {
item {
Row {
Box(
modifier =
Modifier.fillMaxWidth(0.8f)
)
{
Text(
color = Color.Black,
text = stringResource(R.string.location_gps),
fontSize = 30.sp,
modifier = Modifier.padding(20.dp)
)
}
Box(
modifier =
Modifier.fillMaxSize(),
contentAlignment = Alignment.CenterEnd
) {
Switch(
checked = checkedStateGps.value,
onCheckedChange = { checkedStateGps.value = it },
modifier = Modifier
.padding(20.dp),
colors= SwitchDefaults.colors(
//color of switches
checkedThumbColor = Color(0xFF00CC99),
checkedTrackColor = Color(0xFF7BB661),
uncheckedThumbColor = Color(0xFF83010B),
uncheckedTrackColor = Color(0xFFBB4C4C)
)
)
}
}
I'd want to know how can I implement accompanist permissions for this.
In Compose you can use Google's Accompanist library to request permission at runtime, just with PermissionRequired.
This is an example with camera permission but you can request any permissions you have in your manifest file as android.Manifest.permission.*
var doNotShowRationale by rememberSaveable { mutableStateOf(false) }
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
PermissionRequired(
permissionState = cameraPermissionState,
permissionNotGrantedContent = {
if (doNotShowRationale) {
Text("Feature not available")
} else {
Column {
Text("The camera is important for this app. Please grant the permission.")
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Ok!")
}
Spacer(Modifier.width(8.dp))
Button(onClick = { doNotShowRationale = true }) {
Text("Nope")
}
}
}
}
},
permissionNotAvailableContent = {
Column {
Text(
"Camera permission denied. See this FAQ with information about why we " +
"need this permission. Please, grant us access on the Settings screen."
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = navigateToSettingsScreen) {
Text("Open Settings")
}
}
}
) {
Text("Camera permission Granted")
}
Request camera permission sample:
implementation "com.google.accompanist:accompanist-permissions:0.20.0"
The permission APIs are currently experimental and they could change
at any time. All of the APIs are marked with the
#ExperimentalPermissionsApi annotation.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionRequired
import com.google.accompanist.permissions.rememberPermissionState
import pe.edu.upc.permissionscompose.ui.theme.PermissionsComposeTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PermissionsComposeTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
FeatureThatRequiresCameraPermission()
}
}
}
}
}
#OptIn(ExperimentalPermissionsApi::class)
#Composable
fun FeatureThatRequiresCameraPermission() {
var doNotShowRationale by rememberSaveable {
mutableStateOf(false)
}
val cameraPermissionState =
rememberPermissionState(permission = android.Manifest.permission.CAMERA)
val context = LocalContext.current
PermissionRequired(
permissionState = cameraPermissionState,
permissionNotGrantedContent = {
if (doNotShowRationale) {
Text("Feature not available")
} else {
PermissionNotGrantedUI(
onYesClick = {
cameraPermissionState.launchPermissionRequest()
}, onCancelClick = {
doNotShowRationale = true
})
}
},
permissionNotAvailableContent = {
PermissionNotAvailableContent(
onOpenSettingsClick = { context.openSettings() })
},
content = {
Text("Camera Permission Granted")
}
)
}
#Composable
fun PermissionNotAvailableContent(onOpenSettingsClick: () -> Unit) {
Column {
Text("Camera permission denied.")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { onOpenSettingsClick() }) {
Text("Open settings")
}
}
}
#Composable
fun PermissionNotGrantedUI(onYesClick: () -> Unit, onCancelClick: () -> Unit) {
Column {
Text("Camera is important for this app. Please grant ther permission.")
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(onClick = {
onYesClick()
}) {
Text("Yes")
}
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = {
onCancelClick()
}) {
Text("Cancel")
}
}
}
}
Related
I want to add Divider after title. I tried to add Divider(), but it goes to above the text.
I am using Material 3 using implementation "androidx.compose.material3:material3:1.0.1"
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.letsgetchecked.app.common.DialogOptionsData
#Composable
fun <T> DialogOptionsView(
optionData: DialogOptionsData<T>,
) {
AlertDialog(
onDismissRequest = {},
confirmButton = {},
title = {
Text(text = optionData.header)
Divider()
},
text = {
LazyColumn {
items(optionData.items) {
Text(text = "$it")
}
}
},
)
}
#Preview(showBackground = true)
#Composable
fun PreviewDialogOptions() {
val items = listOf(1, 2)
val dataItems = DialogOptionsData(header = "Header", items = items)
DialogOptionsView(dataItems)
}
Expected Output
Actual Output
It happens because the title attribute internally uses a Box as parent container.
Add a Column to achieve the expected result:
AlertDialog(
onDismissRequest = {},
confirmButton = {},
title = {
Column() {
Text(text = "header")
Divider()
}
},
If you use Dialog() composable instead of AlertDialog() you can get full width divider. Try this code
#Composable
fun <T> DialogOptionsView(optionData: DialogOptionsData<T>) {
Dialog(onDismissRequest = {}) {
Surface(shape = RoundedCornerShape(10.dp)) {
Column {
Text(
text = optionData.header,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(16.dp, 14.dp)
)
Divider()
LazyColumn(contentPadding = PaddingValues(16.dp, 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(optionData.items) {
Text(text = "$it")
}
}
}
}
}
}
and the result is
I have implemented 2 bottom sheets in the ModalBottomSheetLayout, both bottom sheets has a list of item checkable with checkbox.
The state of the screen is managed by the viewModel and when the selection changes is invoked a function that copies the state with the new value of the selected text.
When the bottom sheet opens the selection is correct but when I click to change the selection, the bottomsheet is not recomposed and the selection does not change, but in the main screen the state change is correctly read and the value is updated.
Here my code:
MainScreen:
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
#ExperimentalMaterialApi
#Composable
fun MainScreen(
viewModel: MainViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
) {
val screenState = viewModel.screenState
val scope = rememberCoroutineScope()
val bottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden
)
var bottomSheetContent: (#Composable () -> Unit)? by remember {
mutableStateOf(null)
}
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetContent = {
Box(
modifier = Modifier.defaultMinSize(minHeight = 1.dp)
) {
bottomSheetContent?.let { it() }
}
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(text = "First BottomSheet", style = MaterialTheme.typography.h6)
Text(
text = "Selected: ${screenState.selectedTextFromFirstBottomSheet}",
Modifier.padding(16.dp)
)
Button(onClick = {
bottomSheetContent = {
FirstBottomSheet(
selectedText = screenState.selectedTextFromFirstBottomSheet,
onSelected = { text ->
viewModel.onEvent(
MainScreenEvent.OnFirstBottomSheetSelectedTextChanged(text)
)
},
textList = screenState.firstBottomSheetTextList
)
}
scope.launch {
bottomSheetState.show()
}
}, modifier = Modifier.padding(16.dp)) {
Text(text = " Open First BottomSheet")
}
Text(text = "Second BottomSheet", style = MaterialTheme.typography.h6)
Text(
text = "Selected: ${screenState.selectedTextFromSecondBottomSheet}",
Modifier.padding(16.dp)
)
Button(
onClick = {
bottomSheetContent = {
SecondBottomSheet(
selectedText = screenState.selectedTextFromSecondBottomSheet,
onSelected = { text ->
viewModel.onEvent(
MainScreenEvent.OnSecondBottomSheetSelectedTextChanged(text)
)
},
textList = screenState.secondBottomSheetTextList
)
}
scope.launch {
bottomSheetState.show()
}
}, modifier = Modifier
.padding(16.dp)
) {
Text(text = " Open Second BottomSheet")
}
}
}
}
ViewModel:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel(){
var screenState by mutableStateOf(MainScreenState())
fun onEvent(event: MainScreenEvent){
when(event){
is MainScreenEvent.OnFirstBottomSheetSelectedTextChanged -> {
screenState = screenState.copy(
selectedTextFromFirstBottomSheet = event.text
)
}
is MainScreenEvent.OnSecondBottomSheetSelectedTextChanged -> {
screenState = screenState.copy(
selectedTextFromSecondBottomSheet = event.text
)
}
}
}
}
ScreenState
data class MainScreenState(
val selectedTextFromFirstBottomSheet: String = "First Text b1",
val selectedTextFromSecondBottomSheet: String = "Third Text b2",
val firstBottomSheetTextList: List<String> = listOf(
"First Text b1",
"Second Text b1",
"Third Text b1",
"Fourth Text b1",
"Five Text b1"
),
val secondBottomSheetTextList: List<String> = listOf(
"First Text b2",
"Second Text b2",
"Third Text b2",
"Fourth Text b2",
"Five Text b2"
)
)
Screen Event
sealed class MainScreenEvent(){
data class OnFirstBottomSheetSelectedTextChanged(val text: String): MainScreenEvent()
data class OnSecondBottomSheetSelectedTextChanged(val text: String): MainScreenEvent()
}
First Bottom Sheet
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
#Composable
fun FirstBottomSheet(
selectedText: String,
textList: List<String>,
onSelected: (text: String) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
textList.forEach { text ->
Row(modifier = Modifier
.fillMaxWidth()
.toggleable(
value = selectedText == text,
role = Role.Checkbox,
onValueChange = { isSelected ->
if (isSelected) {
onSelected(text)
}
}
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = text, modifier = Modifier.weight(1f))
Checkbox(checked = selectedText == text, onCheckedChange = null)
}
}
}
}
Second Bottom Sheet
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
#Composable
fun SecondBottomSheet(
selectedText: String,
textList: List<String>,
onSelected: (text: String) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
textList.forEach { text ->
Row(modifier = Modifier
.fillMaxWidth()
.toggleable(
value = selectedText == text,
role = Role.Checkbox,
onValueChange = { isSelected ->
if (isSelected) {
onSelected(text)
}
}
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically) {
Text(text = text, modifier = Modifier.weight(1f))
Checkbox(checked = selectedText == text, onCheckedChange = null)
}
}
}
}
Thanks for your help!
I copied and pasted your code. The only changes I made were:
Remove this line from MainScreen
val screenState = viewModel.screenState
Access the state directly.
Text(
text = "Selected: ${viewModel.screenState.selectedTextFromFirstBottomSheet}",
Modifier.padding(16.dp)
)
Button(onClick = {
bottomSheetContent = {
FirstBottomSheet(
selectedText = viewModel.screenState.selectedTextFromFirstBottomSheet,
onSelected = { text ->
viewModel.onEvent(
MainScreenEvent.OnFirstBottomSheetSelectedTextChanged(text)
)
},
textList = viewModel.screenState.firstBottomSheetTextList
)
}
scope.launch {
bottomSheetState.show()
}
}, modifier = Modifier.padding(16.dp)) {
Text(text = " Open First BottomSheet")
}
Boom! It worked :)
My understanding is: you're creating a variable containing the value of a state, but you're not listening to the state changes, so the Compose doesn't know the state has changed, therefore the recomposition doesn't happen. The by keyword in your state declaration is a property delegate which set/get the current value of state, but not register the composable to react to these changes.
There are another solutions you can use to observe the state without repeat viewModel.screenState:
Using derivedStateOf:
val screenState by remember {
derivedStateOf {
viewModel.screenState
}
}
Changing the screenState declaration.
// Using "=" instead of "by"
var screenState = mutableStateOf(MainScreenState())
and then use screenState.value to set/get the state value.
And in the screen, use like below:
val screenState = viewModel.screenState
I had the exact same use case in my app but everything was working fine until I updated some of the compose libraries.
firstly, the code:
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import com.example.saveandloadusername.ui.theme.SaveAndLoadUserNameTheme
import java.io.File
import java.io.IOException
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SaveAndLoadUserNameTheme {
Surface(color = MaterialTheme.colors.background) {
MainScreen(baseContext)
}
}
}
}
}
#Composable
fun MainScreen(context: Context) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
)
{
var name by remember { mutableStateOf("")}
if(checkIfNameIsEmpty(readNameFromInternalStorage(context))) {
Text(text="Hello, give me your name :)")
} else {
Text(text="welcome back ${readNameFromInternalStorage(context)}")
}
Spacer(modifier = Modifier
.height(10.dp)
.fillMaxWidth())
OutlinedTextField(
value=name,
onValueChange={ name = it },
label={Text(text="Name")},
singleLine = true,
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words)
)
Spacer(modifier = Modifier
.height(10.dp)
.fillMaxWidth())
Button(
onClick = {
name = name.replace(" ", "".replace("\n", ""))
if(name == "") {
Toast.makeText(context, "name invalid", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "name (${name}) saved :D", Toast.LENGTH_SHORT).show()
saveNameToInternalStorage(name, context)
}
},
)
{
Text(text="save")
}
Spacer(modifier = Modifier
.height(10.dp)
.fillMaxWidth())
Button(
onClick = {
if(checkIfNameIsEmpty(readNameFromInternalStorage(context))) {
Toast.makeText(context, "you need to give me a name first ;)", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(
context,
"the name is: '${readNameFromInternalStorage(context)}'",
Toast.LENGTH_SHORT
).show()
}
},
)
{
Text(text="check")
}
Spacer(modifier = Modifier
.height(10.dp)
.fillMaxWidth())
Button(onClick = { cleanNameData(context)}) {
Text("Remove name")
}
}
}
private fun saveNameToInternalStorage(name: String, context: Context): Boolean {
return try {
context.applicationContext.openFileOutput("name.txt", MODE_PRIVATE).use { stream ->
stream.flush()
stream.write(name.toByteArray())
Log.i("SAVE_STATE","name ($name) written as ${name.toByteArray()}")
}
return true
} catch(e: IOException) {
e.printStackTrace()
false
}
}
private fun readNameFromInternalStorage(context: Context): String {
val file = File(context.filesDir, "name.txt")
return if (file.exists()) {
val contentOfFile = file.readBytes().decodeToString()
Log.i("CONTENTTT", contentOfFile)
contentOfFile
} else {
""
}
}
private fun checkIfNameIsEmpty(name: String): Boolean {
return name.isEmpty()
}
private fun cleanNameData(context: Context) {
context.applicationContext.openFileOutput("name.txt", MODE_PRIVATE).use { stream ->
stream.flush()
Log.i("cleanNameData", "Name Removed from memory")
Toast.makeText(context, "Name removed", Toast.LENGTH_SHORT).show()
}
}
I've created a little app to get to know android compose and I've stumbled upon a problem that I cannot solve, the Text (that is above the TextField) doesn't update after the buttons "remove name" or "save" are pressed, the text only updates when something changes in the textBox, is there a way to force a recomposition of that text manually?
Any help appreciated :)
Firstly, if all that you need to do is display that name over there, then why take the hassle of storing it to storage, then reading back? Suggestion: You could just use the name parameter as the value of the Text, and upon modification, you could just store the new name to the storage, and if the transaction succeeds, then update the variable. It will trigger the necessary recomposition.
Next up, the reason why the text does not update is because the method you are using to get the name from storage returns a primitive type, not a LiveData. Hence, instead of manually triggering a recompostion, if you are implementing the method yourself, try reading about LiveData first. It should not be difficult to implement.
HOWEVER (not recommended), if all you want is a method to manually re-trigger a composition, then here it is.
As you might know, Compose uses MutableState objects to observe data, which you seem to be very familiar with, since you are already using that. Hence, all you need to do is just add a 'dummy' variable of type MutableState, and then modify it from the onClicks of the buttons in concern. Also, you have to give Compose a message that the dummy variable is being read in the Text, so it would trigger recompostions.
#Composable
fun MainScreen(context: Context) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
)
{
var name by remember { mutableStateOf("")}
var dummy by mutableStateOf(false) // don't even need to remember, long as it compiles
if(checkIfNameIsEmpty(readNameFromInternalStorage(context))) {
dummy
Text(text="Hello, give me your name :)")
} else {
dummy
Text(text="welcome back ${readNameFromInternalStorage(context)}")
}
Spacer(modifier = Modifier
.height(10.dp)
.fillMaxWidth())
OutlinedTextField(
value=name,
onValueChange={ name = it },
label={Text(text="Name")},
singleLine = true,
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words)
)
Spacer(modifier = Modifier
.height(10.dp)
.fillMaxWidth())
Button(
onClick = {
dummy = !dummy // modify to trigger recomposition
name = name.replace(" ", "".replace("\n", ""))
if(name == "") {
Toast.makeText(context, "name invalid", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "name (${name}) saved :D", Toast.LENGTH_SHORT).show()
saveNameToInternalStorage(name, context)
}
},
)
{
Text(text="save")
}
Spacer(modifier = Modifier
.height(10.dp)
.fillMaxWidth())
Button(
onClick = {
dummy = !dummy // The same here
if(checkIfNameIsEmpty(readNameFromInternalStorage(context))) {
Toast.makeText(context, "you need to give me a name first ;)", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(
context,
"the name is: '${readNameFromInternalStorage(context)}'",
Toast.LENGTH_SHORT
).show()
}
},
)
{
Text(text="check")
}
Spacer(modifier = Modifier
.height(10.dp)
.fillMaxWidth())
Button(onClick = {
dummy = !dummy //That's all
cleanNameData(context)
}) {
Text("Remove name")
}
}
}
Yeah this oughta do it.
Hey also, you do not need to paste the entire codebase. Just provide the necessary bits and we'll ask for more if required. Thanks,
In the below code block, you're not using name variable anywhere
if(checkIfNameIsEmpty(readNameFromInternalStorage(context))) {
Text(text="Hello, give me your name :)")
} else {
Text(text="welcome back ${readNameFromInternalStorage(context)}")
}
So, changing name value won't have any effect on recomposition. If you really want, you need to (sort of) have remember{readNameFromInternalStorage(context)}
in order to have recomposition to have effect on Text.
Following the Pathway code labs from Google about Jetpack compose, I was trying out this code
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.BasicsCodelabTheme
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
MyScreenContent()
}
}
}
}
#Composable
fun MyApp(content: #Composable () -> Unit) {
BasicsCodelabTheme {
Surface(color = Color.Yellow) {
content()
}
}
}
#Composable
fun MyScreenContent(names: List<String> = List(1000) { "Hello Android #$it" }) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
NameList(names, Modifier.weight(1f))
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(items = names) { name ->
Greeting(name = name)
Divider(color = Color.Black)
}
}
}
#Composable
fun Greeting(name: String) {
var isSelected by remember { mutableStateOf(false) }
val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
Text(
text = "Hello $name!",
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = { isSelected = !isSelected })
)
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
colors = ButtonDefaults.buttonColors(
backgroundColor = if (count > 5) Color.Green else Color.White
)
) {
Text("I've been clicked $count times")
}
}
#Preview("MyScreen preview")
#Composable
fun DefaultPreview() {
MyApp {
MyScreenContent()
}
}
I have noticed that LazyColumn will recompose the items whenever they become Visible on the screen (intended behaviour!) however, the Local state of Greeting widget is completely lost!
I believe this is a bug in Compose, Ideally the composer should consider the remember cached state. Is there an elegant way to fix this?
Thanks in advance!
[Update]
Using rememberSaveable {...} gives the power to survive the android change of configuration as well as the scrollability
Docs
Remember the value produced by init.
It behaves similarly to remember, but the stored value will survive the activity or process recreation using the saved instance state mechanism (for example it happens when the screen is rotated in the Android application).
The code now is more elegant and shorter, we don't even need to hoist the state, it can be kept internal. The only thing I am not so sure of now with using rememberSaveable is if there will be any performance penalties when the list grows bigger and bigger, say 1000 items.
#Composable
fun Greeting(name: String) {
val isSelected = rememberSaveable { mutableStateOf(false) }
val backgroundColor by animateColorAsState(if (isSelected.value) Color.Red else Color.Transparent)
Text(
text = "Hello $name! selected: ${isSelected.value}",
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = {
isSelected.value = !isSelected.value
})
)
}
[Original Answer]
Based on #CommonWare's answer The LazyColumn will dispose the composables along with their states when they are off-screen this means when LazyColumn recomposes the Compsoables again it will have fresh start state. To fix this issue all that has to be done is to hoist the state to the consumer's scope, LazyColumn in this case.
Also we need to use mutableStateMapOf() instead of MutableMapOf inside the remember { ... } lambda or Compose-core engine will not be aware of this change.
So far here is the code:
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
val selectedStates = remember {
mutableStateMapOf<Int, Boolean>().apply {
names.mapIndexed { index, _ ->
index to false
}.toMap().also {
putAll(it)
}
}
}
LazyColumn(modifier = modifier) {
itemsIndexed(items = names) { index, name ->
Greeting(
name = name,
isSelected = selectedStates[index] == true,
onSelected = {
selectedStates[index] = !it
}
)
Divider(color = Color.Black)
}
}
}
Happy composing!
How should be implemented requesting permission from Jetpack Compose View? I'm trying implement application accessing Camera with Jetpack Compose. I tried example from How to get Current state or context in Jetpack Compose Unfortunately example is no longer working with dev06.
fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
}
}
as compose_version = '1.0.0-beta04' and
implementation 'androidx.activity:activity-compose:1.3.0-alpha06'
you can do request permission as simple as this:
#Composable
fun ExampleScreen() {
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission Accepted: Do something
Log.d("ExampleScreen","PERMISSION GRANTED")
} else {
// Permission Denied: Do something
Log.d("ExampleScreen","PERMISSION DENIED")
}
}
val context = LocalContext.current
Button(
onClick = {
// Check permission
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
) -> {
// Some works that require permission
Log.d("ExampleScreen","Code requires permission")
}
else -> {
// Asking for permission
launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
}
) {
Text(text = "Check and Request Permission")
}
}
Check out Google Accompanist's Jetpack Compose Permissions.
Bear in mind that, at the time of writing, the API is still considered experimental and will require the #ExperimentalPermissionsApi annotation when used.
Documentation and usage: https://google.github.io/accompanist/permissions/
Samples: https://github.com/google/accompanist/tree/main/sample/src/main/java/com/google/accompanist/sample/permissions
Google has a library called "Accompanist". It has many help libraries and one of them is the Permission Library.
Check:
Library: https://github.com/google/accompanist/
Documentation: https://google.github.io/accompanist/permissions/
Example:
Setup in build.gradle file:
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-permissions:<latest_version>"
}
Implementation in Code
#Composable
private fun FeatureThatRequiresCameraPermission() {
// Camera permission state
val cameraPermissionState = rememberPermissionState(
android.Manifest.permission.CAMERA
)
when (cameraPermissionState.status) {
// If the camera permission is granted, then show screen with the feature enabled
PermissionStatus.Granted -> {
Text("Camera permission Granted")
}
is PermissionStatus.Denied -> {
Column {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"The camera is important for this app. Please grant the permission."
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Camera permission required for this feature to be available. " +
"Please grant the permission"
}
Text(textToShow)
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}
}
/**
* Composable helper for permission checking
*
* onDenied contains lambda for request permission
*
* #param permission permission for request
* #param onGranted composable for [PackageManager.PERMISSION_GRANTED]
* #param onDenied composable for [PackageManager.PERMISSION_DENIED]
*/
#Composable
fun ComposablePermission(
permission: String,
onDenied: #Composable (requester: () -> Unit) -> Unit,
onGranted: #Composable () -> Unit
) {
val ctx = LocalContext.current
// check initial state of permission, it may be already granted
var grantState by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(
ctx,
permission
) == PackageManager.PERMISSION_GRANTED
)
}
if (grantState) {
onGranted()
} else {
val launcher: ManagedActivityResultLauncher<String, Boolean> =
rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) {
grantState = it
}
onDenied { launcher.launch(permission) }
}
}
Don't forget to add
<uses-permission android:name="android.permission.CAMERA"/>
(when requesting camera permission) to your AndroidManifest.xml, otherwise it might end up with the permission denied state, when using the solutions others provided.
A little late but this might help as I had the problem today:
With ContextAmbient.current it is not guaranteed that you have an activity or fragment thus I created my own ambient for handling permissions.
val AmbientPermissionHandler: ProvidableAmbient<PermissionHandler> =
ambientOf { throw IllegalStateException("permission handler is not initialized") }
// Activity:
private val permissionHandler = PermissionHandler(this)
// onCreate:
setContent {
Providers(
AmbientPermissionHandler provides permissionHandler
) {/* Composable Contnent */}
Usage:
#Composable
fun PermissionHandler(
permissions: Array<out String>,
requestCode: Int,
granted: #Composable() () -> Unit,
denied: #Composable() () -> Unit,
deniedPermanently: (#Composable() () -> Unit)? = null,
rational: (#Composable() () -> Unit)? = null,
awaitResult: (#Composable() () -> Unit)? = null,
) {
val permissionHandler = AmbientPermissionHandler.current
val (permissionResult, setPermissionResult) = remember(permissions) { mutableStateOf<PermissionResult?>(null) }
LaunchedEffect(Unit) {
setPermissionResult(permissionHandler.requestPermissionsSuspend(requestCode, permissions))
}
when (permissionResult) {
is PermissionResult.PermissionGranted -> granted()
is PermissionResult.PermissionDenied -> denied()
is PermissionResult.PermissionDeniedPermanently -> deniedPermanently?.invoke()
is PermissionResult.ShowRational -> rational?.invoke()
null -> awaitResult?.invoke()
}
}
Implementation of PermissionHandler with dependency https://github.com/sagar-viradiya/eazypermissions
class PermissionHandler(
private val actualHandler: AppCompatActivity,
) {
suspend fun requestPermissionsSuspend(requestCode: Int, permissions: Array<out String>): PermissionResult {
return PermissionManager.requestPermissions(actualHandler, requestCode, *permissions)
}
fun requestPermissionsWithCallback(requestCode: Int, permissions: Array<out String>, onResult: (PermissionResult) -> Unit) {
actualHandler.lifecycleScope.launch {
onResult.invoke(PermissionManager.requestPermissions(actualHandler, requestCode, *permissions))
}
}
}
If you prefer a callback the second function works also.
private const val PERMISSIONS_REQUEST_CODE = 10
private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)
#Composable
fun PermissionButton() {
val context = ContextAmbient.current
Button(onClick = {
if (!hasPermissions(context)) {
requestPermissions(
context as Activity,
PERMISSIONS_REQUIRED,
PERMISSIONS_REQUEST_CODE
)
}
}
) {}
}
fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
You can request multiples permissions.
class MainActivity : ComponentActivity() {
private val neededPermissions = arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
)
#OptIn(ExperimentalMaterialApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MfmTheme {
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions()
) { maps ->
val granted = maps.values.reduce { acc, next -> (acc && next) }
if (granted) {
// all permission granted
} else {
// Permission Denied: Do something
}
// You can check one by one
maps.forEach { entry ->
Log.i("Permission = ${entry.key}", "Enabled ${entry.value}")
}
}
val context = LocalContext.current
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background,
onClick = {
when {
hasPermissions(context, *neededPermissions) -> {
// All permissions granted
}
else -> {
// Request permissions
launcher.launch(neededPermissions)
}
}
}
) {
Greeting("Android")
}
}
}
}
private fun hasPermissions(context: Context, vararg permissions: String): Boolean =
permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
#Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MfmTheme {
Greeting("Android")
}
}
//define permission in composable fun
val getPermission = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
//permission accepted do somthing
} else {
//permission not accepted show message
}
}
//i used SideEffect to launch permission request when screen recomposed
//you can call it inside a button click without SideEffect
SideEffect {
getPermission.launch(Manifest.permission.READ_CONTACTS)
}
and if you wanted to request multiple permission use this:
ActivityResultContracts.RequestMultiplePermissions()
The rememberPermissionState(permission: String) API allows you to request a certain permission to the user and check for the status of the permission.
**Step1:**
A library which provides Android runtime permissions support for Jetpack Compose.
implementation 'com.google.accompanist:accompanist-permissions:0.24.13-rc'
..
**Step2:**
In our AndroidManifeastxml we need to declare permission (in this example we are going to request location permission)
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
..
**Step3:**
In our MainActivity.kt we are calling this permission request function
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Material3ComposeTheme {
RequestPermission(permission = Manifest.permission.ACCESS_FINE_LOCATION)
}
}
}
}
..
**Step4:**
In this SinglePermission.kt, we are going to request permission from user, if user already deny means we will show simple alert dialog info message otherwise will show custom full screen dialog.
package compose.material.theme
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import com.google.accompanist.permissions.*
#ExperimentalPermissionsApi
#Composable
fun RequestPermission(
permission: String,
rationaleMessage: String = "To use this app's functionalities, you need to give us the permission.",
) {
val permissionState = rememberPermissionState(permission)
HandleRequest(
permissionState = permissionState,
deniedContent = { shouldShowRationale ->
PermissionDeniedContent(
rationaleMessage = rationaleMessage,
shouldShowRationale = shouldShowRationale
) { permissionState.launchPermissionRequest() }
},
content = {
/* Content(
text = "PERMISSION GRANTED!",
showButton = false
) {}*/
}
)
}
#ExperimentalPermissionsApi
#Composable
fun HandleRequest(
permissionState: PermissionState,
deniedContent: #Composable (Boolean) -> Unit,
content: #Composable () -> Unit
) {
when (permissionState.status) {
is PermissionStatus.Granted -> {
content()
}
is PermissionStatus.Denied -> {
deniedContent(permissionState.status.shouldShowRationale)
}
}
}
#Composable
fun Content(showButton: Boolean = true, onClick: () -> Unit) {
if (showButton) {
val enableLocation = remember { mutableStateOf(true) }
if (enableLocation.value) {
CustomDialogLocation(
title = "Turn On Location Service",
desc = "Explore the world without getting lost and keep the track of your location.\n\nGive this app a permission to proceed. If it doesn't work, then you'll have to do it manually from the settings.",
enableLocation,
onClick
)
}
}
}
#ExperimentalPermissionsApi
#Composable
fun PermissionDeniedContent(
rationaleMessage: String,
shouldShowRationale: Boolean,
onRequestPermission: () -> Unit
) {
if (shouldShowRationale) {
AlertDialog(
onDismissRequest = {},
title = {
Text(
text = "Permission Request",
style = TextStyle(
fontSize = MaterialTheme.typography.headlineLarge.fontSize,
fontWeight = FontWeight.Bold
)
)
},
text = {
Text(rationaleMessage)
},
confirmButton = {
Button(onClick = onRequestPermission) {
Text("Give Permission")
}
}
)
}
else {
Content(onClick = onRequestPermission)
}
}
..
**Step 5:**
In this CustomDialogLocation.kt, we make custom dialog in android jetpack compose.
package compose.material.theme
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
/*
This example demonstrates how to make custom dialog in android jetpack compose in android.
* Button : https://www.boltuix.com/2021/12/button_25.html
* Clip Modifier : https://www.boltuix.com/2021/12/clip-modifier_24.html
* Alert Dialog : https://www.boltuix.com/2021/12/alert-dialog_25.html
* Column : https://www.boltuix.com/2021/12/column-layout_25.html
* Box : https://www.boltuix.com/2021/12/box-layout_25.html
* Type.kt : https://www.boltuix.com/2021/12/typography_27.html
* Color.kt : https://www.boltuix.com/2022/05/google-material-design-color.html
* Dialog : https://www.boltuix.com/2022/07/compose-custom-animating-dialog.html
* */
#Composable
fun CustomDialogLocation(
title: String? = "Message",
desc: String? = "Your Message",
enableLocation: MutableState<Boolean>,
onClick: () -> Unit
) {
Dialog(
onDismissRequest = { enableLocation.value = false}
) {
Box(
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)
// .width(300.dp)
// .height(164.dp)
.background(
color = MaterialTheme.colorScheme.onPrimary,
shape = RoundedCornerShape(25.dp,5.dp,25.dp,5.dp)
)
.verticalScroll(rememberScrollState())
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
//.........................Image: preview
Image(
painter = painterResource(id = R.drawable.permission_location),
contentDescription = null,
contentScale = ContentScale.Fit,
/* colorFilter = ColorFilter.tint(
color = MaterialTheme.colorScheme.primary
),*/
modifier = Modifier
.padding(top = 5.dp)
.height(320.dp)
.fillMaxWidth(),
)
//.........................Spacer
//.........................Text: title
Text(
text = title!!,
textAlign = TextAlign.Center,
modifier = Modifier
// .padding(top = 5.dp)
.fillMaxWidth(),
letterSpacing = 2.sp,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.height(8.dp))
//.........................Text : description
Text(
text = desc!!,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 10.dp, start = 25.dp, end = 25.dp)
.fillMaxWidth(),
letterSpacing = 1.sp,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary,
)
//.........................Spacer
Spacer(modifier = Modifier.height(24.dp))
//.........................Button : OK button
val cornerRadius = 16.dp
val gradientColors = listOf(Color(0xFFff669f), Color(0xFFff8961))
val roundedCornerShape = RoundedCornerShape(topStart = 30.dp,bottomEnd = 30.dp)
Button(
modifier = Modifier
.fillMaxWidth()
.padding(start = 32.dp, end = 32.dp),
onClick=onClick,
contentPadding = PaddingValues(),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
shape = RoundedCornerShape(cornerRadius)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.horizontalGradient(colors = gradientColors),
shape = roundedCornerShape
)
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text ="Enable",
fontSize = 20.sp,
color = Color.White
)
}
}
//.........................Spacer
Spacer(modifier = Modifier.height(12.dp))
TextButton(onClick = {
enableLocation.value = false
}) { Text("Cancel", style = MaterialTheme.typography.labelLarge) }
Spacer(modifier = Modifier.height(24.dp))
}
}
}
}
Get source code & video: https://www.boltuix.com/2022/07/requesting-location-permission-in.html
There are two ways to get runtime permissions in jetpack compose.
Using activity result
Using the accompanist permissions library
Runtime permission using activity result
The first step is to define the permission in the manifest.xml file.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera"/>
Create an activity result launcher to request the permission we defined. Once it’s launched it will return the result whether the permission is granted or not.
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) {
if (it) {
Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
cameraLauncher.launch(uri)
} else {
Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
}
}
Checking Permission
Before launching the request for permission, we need to check whether the permission is granted or not. If it’s already granted we can proceed with our regular flow. If permission is not provided, then we need to launch the permission request with the permission we wanted.
val permissionCheckResult = ContextCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA)
if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
cameraLauncher.launch(uri)
} else {
permissionLauncher.launch(android.Manifest.permission.CAMERA)
}
finally, the code for the runtime permission using the activity result will be like the below,
val context = LocalContext.current
val file = context.createImageFile()
val uri = FileProvider.getUriForFile(
Objects.requireNonNull(context),
BuildConfig.APPLICATION_ID + ".provider", file
)
var capturedImageUri by remember {
mutableStateOf<Uri>(Uri.EMPTY)
}
val cameraLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
capturedImageUri = uri
}
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) {
if (it) {
Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
cameraLauncher.launch(uri)
} else {
Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
}
}
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(12.dp)) {
Button(onClick = {
val permissionCheckResult = ContextCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA)
if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
cameraLauncher.launch(uri)
} else {
// Request a permission
permissionLauncher.launch(android.Manifest.permission.CAMERA)
}
}) {
Text(text = "Open Camera")
}
if (capturedImageUri.path?.isNotEmpty() == true) {
Image(
modifier = Modifier
.padding(16.dp, 8.dp)
.fillMaxWidth()
.size(400.dp),
painter = rememberImagePainter(capturedImageUri),
contentDescription = null
)
}
}
the output of the above code,