Is there a way to reuse "common" things like DropdownMenu in Compose Multiplatform Project? - kotlin

For the desktop, DropdownMenu is supplied by Gradle: org.jetbrains.compose.material:material-desktop:1.0.1-rc2; for Android it's in Gradle: androidx.compose.material:material:1.1.0-beta04#aar
I would have thought there would be a common API that both implemented.
I know that I could define my own interface/adapter and then plug in the device specific version, but I'm wondering if there is a clever idiomatic Kotlin way to do this.
I tried using 'expect' and 'actual', but I could figure out the syntax. (DropdownMenu doesn't have a simple signature like in the examples of expect/actual usage).
Here's an example of a Menu I'm using... the Android and Desktop versions look identical:
// TODO is there way that this can be moved to Common
#Composable
fun BellSoundMenu(model: SessionViewModel, files: List<SoundFile>) {
val selectedIndex = remember { mutableStateOf(0) }
DropdownMenu(
expanded = model.isBellMenuExpanded.value,
...
) {
files.forEachIndexed { index, sound: SoundFile ->
DropdownMenuItem(onClick = {
selectedIndex.value = index
...
}) {
Row {
val isSelected = sound == model.getBellFile()
Icon(
...
)
Text(
...
)
}
}
}
}
}
I'm looking for a way to move this to the Common folder and not duplicate code.
I ended up doing this; I created an interface:
interface MyDropdown {
#Composable
fun Menu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier,
content: #Composable (androidx.compose.foundation.layout.ColumnScope.() -> Unit)
)
#Composable
fun MenuItem(
onClick: () -> Unit,
content: #Composable (androidx.compose.foundation.layout.RowScope.() -> Unit)
)
}
And then a small object to do the forwarding:
val AndroidDropdown = object : MyDropdown {
#Composable
override fun Menu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier,
content: #Composable (androidx.compose.foundation.layout.ColumnScope.() -> Unit)
) {
return DropdownMenu(
expanded = expanded, onDismissRequest = onDismissRequest, modifier = modifier, content = content
)
}
#Composable
override fun MenuItem(
onClick: () -> Unit, content: #Composable RowScope.() -> Unit
) {
return DropdownMenuItem(onClick = onClick, content = content)
}
}
I'm still curious if there's a more Kotlin-esque way of doing it :)

Related

How can I convert a val to a fun when I use Jetpack Compose in Kotlin?

The Code A is from the official sample project.
I think I use a function instead of the val background, but the Code B is wrong.
How can I convert a val to a fun when I use Jetpack Compose in Kotlin?
Code A
#Composable
fun NiaApp(
windowSizeClass: WindowSizeClass,
appState: NiaAppState = rememberNiaAppState(windowSizeClass)
) {
NiaTheme {
val background: #Composable (#Composable () -> Unit) -> Unit =
when (appState.currentDestination?.route) {
ForYouDestination.route -> { content -> NiaGradientBackground(content = content) }
else -> { content -> NiaBackground(content = content) }
}
background {
Scaffold(
...
) { padding ->
Row(
...
) {
...
}
}
}
}
Code B
#Composable
fun NiaApp(
windowSizeClass: WindowSizeClass,
appState: NiaAppState = rememberNiaAppState(windowSizeClass)
) {
NiaTheme {
#Composable
fun background(aa: #Composable () -> Unit){
when (appState.currentDestination?.route) {
ForYouDestination.route -> { content -> NiaGradientBackground(content = content) }
else -> { content -> NiaBackground(content = content) }
}
}
background {
Scaffold(
...
) { padding ->
Row(
...
) {
...
}
}
}
}
Added content:
To Arpit Shukla: Thanks!
The Code C is based Code A val background: #Composable (#Composable () -> Unit) -> Unit... .
Your Code D is right, but why is Code C wrong ?
Code C
#Composable
fun Background(
appState: NiaAppState,
content: #Composable () -> Unit
) {
when (appState.currentDestination?.route) {
ForYouDestination.route -> { content -> NiaGradientBackground(content = content) }
else -> { content -> NiaBackground(content = content) }
}
}
Code D
#Composable
fun Background(
appState: NiaAppState,
content: #Composable () -> Unit
) {
when (appState.currentDestination?.route) {
ForYouDestination.route -> NiaGradientBackground(content = content)
else -> NiaBackground(content = content)
}
}
Added content again:
To Arpit Shukla: Thanks!
By your way, Code E and Code F can't be compiled.
Code E
#Composable
fun Background(
appState: NiaAppState,
content: #Composable () -> Unit
) {
when (appState.currentDestination?.route) {
ForYouDestination.route -> { content -> NiaGradientBackground(content = content) }
else -> { content -> NiaBackground(content = content) }
}(content) // Call the lambda
}
Code F
#Composable
fun Background(
appState: NiaAppState,
content: #Composable () -> Unit -> NiaGradientBackground(content = content)
) {
when (appState.currentDestination?.route) {
ForYouDestination.route -> { content -> NiaGradientBackground(content = content) }
else -> { content -> NiaBackground(content = content) }
}(content) // Call the lambda
}
New content:
To Arpit Shukla: Thanks!
By your way, Code G can't be compiled yet, I get the following error.
#Composable invocations can only happen from the context of a #Composable function
Code G
#Composable
fun Background(
appState: NiaAppState,
content: #Composable () -> Unit
) {
when (appState.currentDestination?.route) {
ForYouDestination.route -> { content1: #Composable () -> Unit -> NiaGradientBackground(content = content1) }
else -> { content1: #Composable () -> Unit -> NiaBackground(content = content1) }
}(content) // Call the lambda
}
You can try this:
#Composable
fun NiaApp(
windowSizeClass: WindowSizeClass,
appState: NiaAppState = rememberNiaAppState(windowSizeClass)
) {
NiaTheme {
Background(appState) {
Scaffold(
...
) { padding ->
Row(
...
) {
...
}
}
}
}
#Composable
fun Background(
appState: NiaAppState,
content: #Composable () -> Unit
) {
when (appState.currentDestination?.route) {
ForYouDestination.route -> NiaGradientBackground(content = content)
else -> NiaBackground(content = content)
}
}
Edit: Your when statement in Code C only creates a lambda function which when invoked will call the composables. You need to call that lambda too to see any effect:
#Composable
fun Background(
appState: NiaAppState,
content: #Composable () -> Unit
) {
when (appState.currentDestination?.route) {
ForYouDestination.route -> { content -> NiaGradientBackground(content = content) }
else -> { content -> NiaBackground(content = content) }
}(content) // Call the lambda
}
Note: I haven't run this code but the compiler may give you an error here saying that it is unable to infer type for the content variable in the lambda. In that case you will have to explicitly provide the type: content: #Composable () -> Unit -> NiaGradientBackground(content = content)
Anyway, this is too much of unnecessary effort here and is only making the code more complex than the original one. Code D is much straightforward.
Edit: In code G, the lambda is by default not a composable function, you can't call composables inside it. Just putting #Composable in front of the lambda doesn't work, you need to explicitly provide the type for the entire when expression.
#Composable
fun Background(
appState: NiaAppState,
content: #Composable () -> Unit
) {
val background: #Composable (#Composable () -> Unit) -> Unit =
when (appState.currentDestination?.route) {
ForYouDestination.route -> { content -> NiaGradientBackground(content = content) }
else -> { content -> NiaBackground(content = content) }
}
background(content)
}
We reached to the same code we started with which you wanted to simplify. Code D is the best solution in my opinion.
Well, something like this:
#Composable
fun NiaApp(
windowSizeClass: WindowSizeClass,
appState: NiaAppState = rememberNiaAppState(windowSizeClass) ) {
NiaTheme {
background(appState)() {
Scaffold(
...
) { padding ->
}
}
}
}
#Composable
fun background(appState: NiaAppState): #Composable (#Composable () -> Unit) -> Unit =
when (appState.currentDestination?.route) {
ForYouDestination.route -> { content ->
NiaGradientBackground(content = content) }
else -> { content -> NiaBackground(content = content) }
}
just cut your Background composable function and paste it outside of the NiaApp composable function
#Composable
fun NiaApp(
windowSizeClass: WindowSizeClass,
appState: NiaAppState = rememberNiaAppState(windowSizeClass)
) {
NiaTheme {
background {
Scaffold(
...
) { padding ->
Row(
...
) {
...
}
}
}
}
#Composable
fun background(aa: #Composable () -> Unit){
when (appState.currentDestination?.route) {
ForYouDestination.route -> { content ->
NiaGradientBackground(content = content) }
else -> { content -> NiaBackground(content = content) }
}
}

Can I wrap collectAsState with remember when I use Jetpack Compose?

I hope to share a parameter val isCanAddRecord by mViewMode.isCanAddRecord.collectAsState() among #Composable functions.
The Code A is based the article How can I share info among #Composable function in Android Studio?
I know collectAsState() is wrapped with remember, you can see the Source Code.
Now you will find the object watchState is wrapped with remember, and watchState.isCanAddRecord which is assiged to mViewMode.isCanAddRecord.collectAsState() is wrapped with remember again.
Will the Code A cause error?
Code A
#Composable
fun ScreenHome_Watch(
modifier: Modifier = Modifier,
mViewMode: SoundViewModel,
watchState:WatchState = rememberWatchState(mViewMode)
){
...
}
class WatchState(
val isCanAddRecord: State<Boolean>,
...
){
...
}
#Composable
fun rememberWatchState(mViewMode: SoundViewModel): WatchState {
val watchState = WatchState(mViewMode.isCanAddRecord.collectAsState())
return remember {
watchState
}
}
Source Code
#Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
#Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
Of course you can remember your mutableState or any other remember or anything that is not Composable. You can remember measurePolicy or even another code block as lambda for drawing like Modifier.drawWithCache does. Jetnews App sample does what you about. This is a matter of preference, you can store anything that is not Composable inside your remember block.
/**
* Remembers the content for each tab on the Interests screen
* gathering application data from [InterestsViewModel]
*/
#Composable
fun rememberTabContent(interestsViewModel: InterestsViewModel): List<TabContent> {
// UiState of the InterestsScreen
val uiState by interestsViewModel.uiState.collectAsState()
// Describe the screen sections here since each section needs 2 states and 1 event.
// Pass them to the stateless InterestsScreen using a tabContent.
val topicsSection = TabContent(Sections.Topics) {
val selectedTopics by interestsViewModel.selectedTopics.collectAsState()
TabWithSections(
sections = uiState.topics,
selectedTopics = selectedTopics,
onTopicSelect = { interestsViewModel.toggleTopicSelection(it) }
)
}
val peopleSection = TabContent(Sections.People) {
val selectedPeople by interestsViewModel.selectedPeople.collectAsState()
TabWithTopics(
topics = uiState.people,
selectedTopics = selectedPeople,
onTopicSelect = { interestsViewModel.togglePersonSelected(it) }
)
}
val publicationSection = TabContent(Sections.Publications) {
val selectedPublications by interestsViewModel.selectedPublications.collectAsState()
TabWithTopics(
topics = uiState.publications,
selectedTopics = selectedPublications,
onTopicSelect = { interestsViewModel.togglePublicationSelected(it) }
)
}
return listOf(topicsSection, peopleSection, publicationSection)
}
val tabContent = rememberTabContent(interestsViewModel)
val (currentSection, updateSection) = rememberSaveable {
mutableStateOf(tabContent.first().section)
}
Remember lambda
fun Modifier.drawWithCache(
onBuildDrawCache: CacheDrawScope.() -> DrawResult
) = composed(
inspectorInfo = debugInspectorInfo {
name = "drawWithCache"
properties["onBuildDrawCache"] = onBuildDrawCache
}
) {
val cacheDrawScope = remember { CacheDrawScope() }
this.then(DrawContentCacheModifier(cacheDrawScope, onBuildDrawCache))
}
Remember layout policy which is widely used with layouts
#Composable
#UiComposable
fun BoxWithConstraints(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content:
#Composable #UiComposable BoxWithConstraintsScope.() -> Unit
) {
val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
SubcomposeLayout(modifier) { constraints ->
val scope = BoxWithConstraintsScopeImpl(this, constraints)
val measurables = subcompose(Unit) { scope.content() }
with(measurePolicy) { measure(measurables, constraints) }
}
}

Preview a "screen" in android jetpack compose navigation with a PreviewParameter NavController

I discovering android Cetpack Compose (and Navigation) and try to display a preview of a view with a navController as parameter.
To achieve, I use the PreviewParameter and I have no error, but nothing displayed in the Preview window.
Anyone know how pass a fake NavController instance to a Composable?
class FakeNavController : PreviewParameterProvider<NavController> {
override val values: Sequence<NavController>
get() {}
}
#Preview
#Composable
fun Preview(
#PreviewParameter(FakeNavController::class) fakeNavController: NavController
) {
HomeView(fakeNavController)
}
You don't have to make it nullable and pass null to it.
You just need to pass this: rememberNavController()
#Preview
#Composable
fun Preview() {
HomeView(rememberNavController())
}
PreviewParameter is used to create multiple previews of the same view with different data. You're expected to return the needed values from values. In your example you return nothing that's why it doesn't work(it doesn't even build in my case).
That won't help you to mock the navigation controller, as you still need to create it somehow to return from values. I think it's impossible.
Instead you can pass a handler, in this case you don't need to mock it:
#Composable
fun HomeView(openOtherScreen: () -> Unit) {
}
// your Navigation view
composable(Destinations.Home) { from ->
HomeView(
openOtherScreen = {
navController.navigate(Destinations.Other)
},
)
}
Finally, I declare a nullable NavController and it works.
#Composable
fun HomeView(navController: NavController?) {
Surface {
Column(
modifier = Modifier
.padding(all = 4.dp)
) {
Text(
text = "Home View",
style = MaterialTheme.typography.body2
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { navController?.navigate("lineRoute") }) {
Text(text = "nav to Line view")
}
}
}
}
#Preview
#Composable
fun Preview (){
HomeView(null)
}

What the differents variables will be destructured when I use rememberSaveable?

The Code A is from offical sample code here.
The code val (currentSection, updateSection) = rememberSaveable { mutableStateOf(tabContent.first().section) } will create two variables, one is currentSection, another is updateSection.
According to the Hint of Android Studio, I find the following definition
val currentSection: Sections
val updateSection: (Sections) → Unit
I read the source code of both tabContent and rememberSaveable, but I can't understand why the rememberSaveable can destructure it to two different variables (Sections And (Sections) → Unit). Why can't the rememberSaveable destructure it to three different variables with other types?
Code A
#Composable
fun InterestsRoute(
interestsViewModel: InterestsViewModel,
isExpandedScreen: Boolean,
openDrawer: () -> Unit,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
val tabContent = rememberTabContent(interestsViewModel)
val (currentSection, updateSection) = rememberSaveable {
mutableStateOf(tabContent.first().section)
}
InterestsScreen(
tabContent = tabContent,
currentSection = currentSection,
isExpandedScreen = isExpandedScreen,
onTabChange = updateSection,
openDrawer = openDrawer,
scaffoldState = scaffoldState
)
}
#Composable
fun rememberTabContent(interestsViewModel: InterestsViewModel): List<TabContent> {
...
return listOf(topicsSection, peopleSection, publicationSection)
}
class TabContent(val section: Sections, val content: #Composable () -> Unit)
The destructing that you are referring to has actually nothing to do with rememberSaveable.
The rememberSaveable { mutableStateOf(...) } function returns a MutableState and this is what can be destructured.
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
Here you can see the two components that you are referring to ( T and (T) -> Unit)

Kotlin #Composable invocations can only happen from the context of a #Composable function

I have an issue with MyApp function, content value is unresolved and for ContactContent() shows this error: #Composable invocations can only happen from the context of a #Composable function
#Composable
fun MyApp(navigateToProfile: (Contact) -> Unit){
Scaffold {
content = {
ContactContent(navigateToProfile = navigateToProfile)
}
}
}
ContactContent Snippet
#Composable
fun ContactContent(navigateToProfile: (Contact) -> Unit) {
val contacts = remember { DataProvider.contactList }
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
items(
items = contacts,
itemContent = {
ContactListItem(contact = it, navigateToProfile)
}
)
}
}
You are already in a Scaffold's body. U don't need to use content = {}
Change to:
#Composable
fun MyApp(navigateToProfile: (Contact) -> Unit){
Scaffold {
ContactContent(navigateToProfile = navigateToProfile)
}
}
content is a parameter of Scaffold If you want to use it:
#Composable
fun MyApp(navigateToProfile: (Contact) -> Unit){
Scaffold(
content = {
ContactContent(navigateToProfile = navigateToProfile)
}
)
}
both work the same way.