How can I convert a val to a fun when I use Jetpack Compose in Kotlin? - 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) }
}
}

Related

Can I use mutableStateOf() instead of produceState in Compose?

The Code A is from the official sample project.
I don't think the produceState is necessary, so I think I can replace Code A with Code B, is it right?
BTW, the Code B can run.
Code A
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
...
}
else -> { onErrorLoading() }
}
}
Code B
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
var uiState by remember {mutableStateOf(DetailsUiState(isLoading = true))}
val cityDetailsResult = viewModel.cityDetails
uiState = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
...
}
else -> { onErrorLoading() }
}
}
Let's take a look at productState under the hood:
#Composable
fun <T> produceState(
initialValue: T,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(Unit) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
productState without a key in it's arguments, uses LaunchedEffect with Unit key which Create an effect that matches the lifecycle of the call site.
It means if DetailsScreen recomposes, the code that provides uiState won't start again.
But In code B, you are just remembering DetailsUiState across recomposition, and below line will be executed in every recomposition.
val cityDetailsResult = viewModel.cityDetails
uiState = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}

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

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 :)

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.

Builder pattern with infix for Coroutines

I am trying to write a class to easily chain code run in different coroutine contexts.
Ideally, I would like to use it like this:
io {
// Run IO code that returns an object (nullable)
} ui { ioResult->
// Run UI code using the returned object (non-nullable)
} ifNull {
// Run UI code when the returned object is null
}
What I have so far works like this:
GlobalScope.launch {
CoroutineLinker(null).io {
// Run IO code
} ui { ioResult ->
ioResult?.also {
// Run UI code after null check
} ?: run {
// Run UI code when null
}
} ifNull {
// Redundant block
}
}
As you can see there is still quite some work left but I am stuck, so I share this with you:
class CoroutineLinker<T> (
private val value: T?
) {
suspend infix fun <K> io (block: suspend () -> K?): CoroutineLinker<K?> {
return withContext(Dispatchers.IO) {
CoroutineLinker(block())
}
}
suspend infix fun ui (block: suspend (value: T) -> Unit): CoroutineLinker<T> {
return withContext(Dispatchers.Main) {
if (value != null ) {
block(value)
}
this#CoroutineLinker
}
}
suspend infix fun ifNull (block: suspend () -> Unit) {
return withContext(Dispatchers.Main) {
if (value == null) {
block()
}
}
}
}
Any input is welcome! :)
I think this will do what you need:
suspend fun <K : Any> io (block: suspend () -> K?) = CoroutineLinker(null).io(block)
class CoroutineLinker<T : Any> (
private val value: T?
) {
suspend infix fun <K : Any> io (block: suspend () -> K?): CoroutineLinker<K> {
return withContext(Dispatchers.IO) {
CoroutineLinker(block())
}
}
suspend infix fun ui (block: suspend (value: T) -> Unit): CoroutineLinker<T> {
if (value != null ) {
withContext(Dispatchers.Main) {
block(value)
}
}
return this
}
suspend infix fun ifNull (block: suspend () -> Unit) {
if (value == null) {
withContext(Dispatchers.Main) {
block()
}
}
}
}
I changed 3 things:
Added upper bounds for CoroutineLinker to Any.
Added io function.
Changed the order of if and withContext in both functions - this is just optimization, it wasn't required.

How to execute a defined function after each function in Kotlin

I am writing Espresso unit test code.
What I want to do is taking screenshot on every actions without specifying
takeSpoonScreenshot("")
This is my AndroidJUnit4 Testcode:
#Test
fun givenVideoDetail_whenChooseCurrentItem_thenShowCountLabel() {
pickerPage {
clickFirstVideoItem()
}
videoDetailPage {
clickSelectCheckBox()
assertCountLabel()
}
}
and this is my VideoDetailPage.kt:
fun videoDetailPage(func: VideoDetailPage.() -> Unit) = VideoDetailPage.apply {
assertFirstPage()
func()
}
fun screenshotAfterAction(func: VideoDetailPage.() -> Unit) = VideoDetailPage.apply {
func()
takeSpoonScreenshot("")
}
object VideoDetailPage : BaseActions() {
// Write 'How to test' here
fun assertFirstPage() {
resourceIsDisplayed(R.id.send_balloon_image)
resourceIsDisplayed(R.id.media_detail_item_check_box)
resourceIsDisplayed(R.id.video_editor_mute_btn)
}
fun clickFilterButton() = takeScreenshotAfterFunction {
clickButton(R.id.image_editor_filter)
}
fun clickSelectCheckBox() {
clickButton(R.id.media_detail_item_check_box)
}
fun assertFilterSelectionListIsOpen() {
resourceIsDisplayed(R.id.media_filter_list)
}
fun assertCountLabel() {
resourceIsDisplayed(R.id.media_editor_selected_count)
}
}
See that I made takeScreenshotAfterFunction, but It is not proper because I should write takeScreenshotAfterFunction N times.