What i am trying to do is to close the TopBar dropdown menu after clicking the dropdown item. It can be easily done, if i am putting the dropdown items directly inside the dropdown menu. But here i am trying to separate it as a composable for readability.
Here is my TopAppBar
#Composable
fun TopBar(
scope: CoroutineScope,
scaffoldState: ScaffoldState,
event: (AdminLaunchEvents) -> Unit,
navController: NavHostController
) {
val openDialog = remember { mutableStateOf(false) }
TopAppBar(
title = {
Text(text = "Main App Admin Area", fontSize = 18.sp)
},
actions = {
OverflowMenu() {
SettingsDropDownItem(onClick = {})
ModeDropDownItem(onClick = {})
LogoutDropDownItem(onClick = {
openDialog.value = true
})
}
},
backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.White
)
if (openDialog.value) {
LogOutComponent(openDialog = openDialog, event = event,navController = navController)
}
}
And this is the OverFlowMenu composable which contains the DropDown Menu
#Composable
fun OverflowMenu(content: #Composable () -> Unit) {
var showMenu by remember { mutableStateOf(false) }
IconButton(onClick = {
showMenu = !showMenu
}) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = "More",
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
content()
}
}
Now given below is the DropDownItem.
#Composable
fun SettingsDropDownItem(onClick: () -> Unit) {
DropdownMenuItem(onClick = onClick) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Settings")
}
}
What i am trying to do is, when i click the SettingsDroDownItem, i need to capture the click event in the OverFlowMenu composable to make the showMenu false, so as the hide the DropdownMenu. I can get the click event in the TopAppBar, but how to get it on DropDownMenu.
How to do that?
The first option is moving showMenu state out of OverflowMenu, as this is not the only composable which depends on the value. Something like this:
OverFlowMenu:
#Composable
fun OverflowMenu(showMenu: Bool, setShowMenu: (Bool) -> Unit, content: #Composable () -> Unit) {
// ...
}
TopBar:
actions = {
var (showMenu, setShowMenu) = remember { mutableStateOf(false) }
OverflowMenu(showMenu, setShowMenu) {
SettingsDropDownItem(onClick = {
openDialog.value = true
setShowMenu(false)
})
}
},
An other options is creating something like OverflowMenuScope, and running SettingsDropDownItem on this scope so it can close the menu itself:
OverflowMenu:
interface OverflowMenuScope {
fun closeMenu()
}
#Composable
fun OverflowMenu(content: #Composable OverflowMenuScope.() -> Unit) {
var showMenu by remember { mutableStateOf(false) }
val scope = remember {
object: OverflowMenuScope {
override fun closeMenu() {
showMenu = false
}
}
}
//...
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
scope.content()
}
}
SettingsDropDownItem:
#Composable
fun OverflowMenuScope.SettingsDropDownItem(onClick: () -> Unit) {
DropdownMenuItem(onClick = {
closeMenu()
onClick()
}) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Settings")
}
}
Related
I was developing an app where I'm using Jetpack Compose as UI developing tool, and I design some kind of custom AlertDialog, which is the following:
CustomAlertDialog.kt
#Composable
fun SimpleAlertDialog(
hero: CharacterModel? = null,
show: Boolean,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
textDescription: String,
textTittle: String,
) {
if(show){
AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = onConfirm)
{ Text(text = "OK") }
},
dismissButton = {
TextButton(onClick = onDismiss)
{ Text(text = "Cancel") }
},
title = { Text(text = textTittle) },
text = { Text(text = textDescription) }
)
}
}
But when I try to use in a detailScreen, I get the following context Composable error:
#Composable invocations can only happen from the context of a #Composable function
the region of code where I try to instances the following, where I get the error:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun MyCharactersListRowView(
viewmodel: MainViewModel,
characterModel: CharacterModel,
popBack: () -> Unit
) {
val (characterSelected, setCharacterSelected) = remember { mutableStateOf<CharacterModel?>(null) } //HOOK FUNCTION
val openDialog = remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = { setCharacterSelected(characterModel) })
.padding(vertical = 8.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
){
AsyncImage(
model = characterModel.image,
contentDescription = characterModel.name,
contentScale = ContentScale.Fit,
modifier = Modifier
.size(60.dp)
.clip(CircleShape)
.combinedClickable(
onLongClick = {
if (characterSelected != null) {
SimpleAlertDialog(
hero = characterSelected,
show = true,
onConfirm = {
viewmodel
.deleteCharacter(characterSelected!!.id.toLong())
.also {
Log.d(
"info",
"rowAffected: ${viewmodel.rowAffected.value}"
)
if (viewmodel.rowAffected.value.toInt() != 0) {
Toast
.makeText(
LocalContext.current!!,
"ยก ${characterSelected.name} bought sucessfully!",
Toast.LENGTH_LONG
)
.show()
.also {
openDialog.value = false
}
} else {
Toast
.makeText(
LocalContext.current!!,
viewmodel.loadError.value,
Toast.LENGTH_LONG
)
.show()
.also {
openDialog.value = false
}
}
}
},
onDismiss = { openDialog.value = false },
textTittle = "Remove character",
textDescription = "Would you like to remove ${characterSelected.name} from your characters?"
)
} else {
}
},
onClick = {
//TODO {Do something}
}
)
)
...
...
So I know is a quite beginner error, but I've not been able to get into a working solution, due to take thanks in advance is you know how implement it.
You can't call a Dialog from a lambda that doesn't have #Composable annotation. You can check this answer out for differences between Composable and non-Composable functions.
fun Modifier.combinedClickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onLongClickLabel: String? = null,
onLongClick: (() -> Unit)? = null,
onDoubleClick: (() -> Unit)? = null,
onClick: () -> Unit
)
These lambdas are not #Composable
Common way for showing dialog in Jetpack Compose is
var showDialog by remember {mutableStateOf(false)}
if(characterSelected && showDialog) {
SimpleAlertDialog(onDismiss={showDialog = false})
}
and change showDialog to true inside long click
onLongClick = {
showDialog = true
}
Show custom alert dialog in Jetpack Compose
I have a custom search bar will not allow me to place a cursor and type text into it. Before adding the viewmodel and state, I was able to have the TextField work and allow typing text into it.
I've tried using state variables within the composable instead of separating the logic into the view model and unfortunatly recieved the same result. I have a feeling it's something simple that I'm missing but can't quite find it.
Custom Search Bar:
#Composable
fun SearchBar(
modifier: Modifier,
viewModel: ToolSetListViewModel = hiltViewModel()
){
Surface (
modifier = modifier
.fillMaxWidth()
.height(74.dp)
.padding(20.dp, 15.dp, 20.dp, 0.dp),
elevation = 10.dp,
color = MaterialTheme.colors.primary,
shape = RoundedCornerShape(25)
){
TextField(
modifier = modifier
.fillMaxWidth(),
value = viewModel.searchText,
onValueChange = {
viewModel.onEvent(ToolSetListEvent.OnSearchToolSet(it))
},
placeholder = {
Text(
modifier = modifier
.alpha(ContentAlpha.medium),
text = "Search...",
color = White
)
},
textStyle = TextStyle(
fontSize = MaterialTheme.typography.subtitle1.fontSize,
),
singleLine = true,
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
cursorColor = White
)
)
}
Screen:
#Composable
fun ToolSetListScreen(
onNavigate: (UiEvent.Navigate) -> Unit,
viewModel: ToolSetListViewModel = hiltViewModel()
) {
val toolSets = viewModel.toolSets.collectAsState(initial = emptyList())
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when(event) {
is UiEvent.Navigate -> onNavigate(event)
}
}
}
Scaffold (
floatingActionButton = {
FloatingActionButton(onClick = {
viewModel.onEvent(ToolSetListEvent.OnAddToolSetClick)
}) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add")
}
}
) {
SearchBar(
modifier = Modifier,
)
LazyColumn(
modifier = Modifier.fillMaxSize()
.padding(0.dp, 20.dp)
) {
items(toolSets.value) { toolset ->
ToolSetItem(
toolSet = toolset,
onEvent = viewModel::onEvent,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clickable {
viewModel.onEvent(ToolSetListEvent.OnToolSetClick(toolset))
}
)
}
}
}
}
ViewModel:
#HiltViewModel
class ToolSetListViewModel #Inject constructor(
private val repository: ToolSetRepository
): ViewModel() {
val toolSets = repository.getAllToolSets()
private val _uiEvent = Channel<UiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()
var searchText by mutableStateOf("")
private set
fun onEvent(event: ToolSetListEvent) {
when(event) {
is ToolSetListEvent.OnToolSetClick -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TOOL_SET + "?PONumber=${event.toolSet.PONumber}"))
}
is ToolSetListEvent.OnDeleteToolSetClick -> {
viewModelScope.launch {
repository.deleteToolSet(event.toolset)
}
}
is ToolSetListEvent.OnAddToolSetClick -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TOOL_SET))
}
is ToolSetListEvent.OnSearchToolSet -> {
viewModelScope.launch {
if (event.searchText.isNotBlank()) {
searchText = event.searchText
repository.getToolSetByPO(event.searchText)
}
}
}
}
}
private fun sendUiEvent(event: UiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}
MainActivity:
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PunchesManagerTheme {
val navController = rememberNavController()
Scaffold (
content = {
Navigation(navController)
},
bottomBar = { BottomNavigationBar(navController = navController) },
)
}
}
}
}
#Composable
fun BottomNavigationBar(navController: NavHostController) {
BottomNavigation {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = backStackEntry?.destination?.route
NavBarItems.bottomNavItem.forEach { navItem ->
BottomNavigationItem(selected = currentRoute == navItem.route, onClick = {
navController.navigate(navItem.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = false
}
launchSingleTop = true
restoreState = true
}
},
icon = {
Icon(imageVector = navItem.icon,
contentDescription = navItem.name)
},
label = {
Text(text = navItem.name)
},
)
}
}
}
Any suggestions on what the issue may be?
I think I found the fix!
I adjusted the layout of my scaffoled in my ToolSetListScreen:
Scaffold (
content = {
SearchBar(
modifier = Modifier,
)
LazyColumn(
modifier = Modifier.fillMaxSize()
.padding(0.dp, 75.dp)
) {
items(toolSets.value) { toolset ->
ToolSetItem(
toolSet = toolset,
onEvent = viewModel::onEvent,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clickable {
viewModel.onEvent(ToolSetListEvent.OnToolSetClick(toolset))
}
)
}
}
},
floatingActionButton = {
FloatingActionButton(onClick = {
viewModel.onEvent(ToolSetListEvent.OnAddToolSetClick)
}) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add")
}
}
)
}
and this seemed to work.
#Composable
fun TopAppBar(
title: #Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: #Composable (() -> Unit)? = null,
actions: #Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation
)
actions: #Composable RowScope.() -> Unit = {}
Usage Scenario:
Using Compose Navigation to switch to different "screens", so the TopAppBar actions will be changed accordingly. Eg. Share buttons for content screen, Filter button for listing screen
Tried passing as a state to the TopAppBar's actions parameter, but having trouble to save the lambda block for the remember function.
val (actions, setActions) = rememberSaveable { mutableStateOf( appBarActions ) }
Want to change the app bar actions content dynamically. Any way to do it?
This the approach I used but I'm pretty new on compose, so I cannot be sure it is the correct approach.
Let's assume I have 2 screens: ScreenA and ScreenB
They are handled by MainActivity screen.
This is our MainActivity:
#ExperimentalComposeUiApi
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CoolDrinksTheme {
val navController = rememberNavController()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var appBarState by remember {
mutableStateOf(AppBarState())
}
Scaffold(
topBar = {
SmallTopAppBar(
title = {
Text(text = appBarState.title)
},
actions = {
appBarState.actions?.invoke(this)
}
)
}
) { values ->
NavHost(
navController = navController,
startDestination = "screen_a",
modifier = Modifier.padding(
values
)
) {
composable("screen_a") {
ScreenA(
onComposing = {
appBarState = it
},
navController = navController
)
}
composable("screen_b") {
ScreenB(
onComposing = {
appBarState = it
},
navController = navController
)
}
}
}
}
}
}
}
}
As you can see I'm using a mutable state of a class which represents the state of our MainActivity (where the TopAppBar is declared and composed), in this example there is the title and the actions of our TopAppBar.
This mutable state is set with a callback function called inside the composition of each screen.
Here you can see the ScreenA
#Composable
fun ScreenA(
onComposing: (AppBarState) -> Unit,
navController: NavController
) {
LaunchedEffect(key1 = true) {
onComposing(
AppBarState(
title = "My Screen A",
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null
)
}
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Filter,
contentDescription = null
)
}
}
)
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Screen A"
)
Button(onClick = {
navController.navigate("screen_b")
}) {
Text(text = "Navigate to Screen B")
}
}
}
And the ScreenB
#Composable
fun ScreenB(
onComposing: (AppBarState) -> Unit,
navController: NavController
) {
LaunchedEffect(key1 = true) {
onComposing(
AppBarState(
title = "My Screen B",
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null
)
}
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null
)
}
}
)
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Screen B"
)
Button(onClick = {
navController.popBackStack()
}) {
Text(text = "Navigate back to Screen A")
}
}
}
And finally this is the data class of our state:
data class AppBarState(
val title: String = "",
val actions: (#Composable RowScope.() -> Unit)? = null
)
In this way you have a dynamic appbar declared in the main activity but each screen is responsable to handle the content of the appbar.
First you need to add navigation dependency on you jetpack compose projects.
You can read the doc from this https://developer.android.com/jetpack/compose/navigation
def nav_version = "2.4.1"
implementation "androidx.navigation:navigation-compose:$nav_version"
Then define your screen in sealed class:
sealed class Screen(var icon: ImageVector, var route: String) {
object ContentScreen: Screen(Icons.Default.Home, "home")
object ListingScreen: Screen(Icons.Default.List, "list")
}
and this is the navigation function look like
#Composable
fun Navigation(paddingValues: PaddingValues, navController: NavHostController) {
NavHost(navController, startDestination = Screen.ContentScreen.route, modifier = Modifier.padding(paddingValues)) {
composable(Screen.ContentScreen.route) {
//your screen content
}
composable(Screen.ListingScreen.route) {
//your listing screen here
}
}
}
Finally in your mainactivity class
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestAppTheme {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
topBar = {
TopAppBar(title = { Text(text = "main screen") }, actions = {
if (currentRoute == Screen.ContentScreen.route) {
//your share button action here
} else if (currentRoute == Screen.ListingScreen.route) {
//your filter button here
} else {
//other action
}
})
}
) {
Navigation(paddingValues = it, navController = navController)
}
}
}
}
I'm so sorry if the explanation to sort, because the limitation of my English
I have this composable function that a button will toggle show the text and hide it
#Composable
fun Greeting() {
Column {
val toggleState = remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState.value) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {}
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
One thing I didn't like the code is val toggleState = remember { ... }.
I prefer val toggleState by remember {...}
However, if I do that, as shown below, I cannot pass the toggleState over to ToggleButton, as ToggleButton wanted mutableState<Boolean> and not Boolean. Hence it will error out.
#Composable
fun Greeting() {
Column {
val toggleState by remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {} // Here will have error
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
How can I fix the above error while still using val toggleState by remember {...}?
State hoisting in Compose is a pattern of moving state to a composable's caller to make a composable stateless. The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
You can do something like
// stateless composable is responsible
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggle: Boolean,
onToggleChange: () -> Unit) {
TextButton(
onClick = onToggleChange,
modifier = modifier
)
{ Text(text = if (toggle) "Stop" else "Start") }
}
and
#Composable
fun Greeting() {
var toggleState by remember { mutableStateOf(false) }
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggle = toggleState,
onToggleChange = { toggleState = !toggleState }
)
}
You can also add the same stateful composable which is only responsible for holding internal state:
#Composable
fun ToggleButton(modifier: Modifier = Modifier) {
var toggleState by remember { mutableStateOf(false) }
ToggleButton(modifier,
toggleState,
onToggleChange = {
toggleState = !toggleState
},
)
}
How can menu icons of Toolbar can be turned into overflow in Compose?
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
},
actions = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite)
}
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Refresh)
}
IconButton(
onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Call)
}
}
)
},
bottomBar = {
BottomNavigationLayout()
}
) { innerPadding ->
PhotoCard(Modifier.padding(innerPadding))
}
I want only one of the icons in Toolbar menu to be visible while others to be added to overflow menu like done with xml using app:showAsAction="never"
<item
android:id="#+id/action_sign_out"
android:title="#string/toolbar_sign_out"
app:showAsAction="never"/>
You have to provide the OverFlowMenu yourself, e.g.:
#Preview
#Composable
fun PreviewOverflowMenu() {
OverflowMenuTest()
}
#Composable
fun OverflowMenuTest() {
var showMenu by remember { mutableStateOf(false) }
TopAppBar(
title = { Text("Title") },
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Default.Favorite)
}
IconButton(onClick = { showMenu = !showMenu }) {
Icon(Icons.Default.MoreVert)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Refresh)
}
DropdownMenuItem(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Call)
}
}
}
)
}
Edit: Updated for Compose 1.0.0-beta08
I modified a bit #jns's answer to make it more modular and reusable.
This is the reusable OverflowMenu:
#Composable
fun OverflowMenu(content: #Composable () -> Unit) {
var showMenu by remember { mutableStateOf(false) }
IconButton(onClick = {
showMenu = !showMenu
}) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.more),
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
content()
}
}
And this is how it is used inside the TopAppBar:
TopAppBar(
title = {
Text(text = stringResource(R.string.my_title))
},
actions = {
OverflowMenu {
DropdownMenuItem(onClick = { /*TODO*/ }) {
Text("Settings")
}
DropdownMenuItem(onClick = { /*TODO*/ }) {
Text("Bookmarks")
}
}
}
)
We can possibly add icons to DropDownMenuItems if desired. And these items can be extracted as reusable composables as well. If there are other action buttons that you want to show as iconified buttons on the menu(i.e. show as action), you should put them before the OverflowMenu.
TopAppBar(
title = {
Text(text = stringResource(R.string.bookmark))
},
actions = {
//This icon will be shown on the top bar, on the left of the overflow menu
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.FavoriteBorder, stringResource(R.string.cd_favorite_item))
}
OverflowMenu {
SettingsDropDownItem(onClick = { /*TODO*/ })
BookmarksDropDownItem(onClick = { /*TODO*/ })
}
}
)
.
#Composable
fun SettingsDropDownItem(onClick : () -> Unit) {
//Drop down menu item with an icon on its left
DropdownMenuItem(onClick = onClick) {
Icon(Icons.Filled.Settings,
contentDescription = stringResource(R.string.settings),
modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.settings))
}
}
#Composable
fun BookmarksDropDownItem(onClick : () -> Unit) {
//Drop down menu item with an icon on its left
DropdownMenuItem(onClick = onClick) {
Icon(painter = painterResource(R.drawable.ic_bookmark_filled),
contentDescription = stringResource(R.string.bookmark),
modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.bookmark))
}
}
Inspired by #jns's answer, I made an ActionMenu composable which takes a list of ActionItemSpec objects. and displays them with an overflow menu if necessary. I modelled the ActionItemSpec a bit like the old XML menu item entries, but added an onClick lambda.
It's used like this
#Preview
#Composable
fun PreviewActionMenu() {
val items = listOf(
ActionItemSpec("Call", Icons.Default.Call, ActionItemMode.ALWAYS_SHOW) {},
ActionItemSpec("Send", Icons.Default.Send, ActionItemMode.IF_ROOM) {},
ActionItemSpec("Email", Icons.Default.Email, ActionItemMode.IF_ROOM) {},
ActionItemSpec("Delete", Icons.Default.Delete, ActionItemMode.IF_ROOM) {},
)
TopAppBar(
title = { Text("App bar") },
navigationIcon = {
IconButton(onClick = {}) {
Icon(Icons.Default.Menu, "Menu")
}
},
actions = {
// show 3 icons including overflow
ActionMenu(items, defaultIconSpace = 3)
}
)
}
and the preview looks like this
Full pastebin is here: https://gist.github.com/MachFour/369ebb56a66e2f583ebfb988dda2decf