https://kotlinlang.org logo
#compose
Title
# compose
r

Reprator

08/25/2020, 5:40 AM
How to sort this issue, @Composable invocations can only happen from the context of a @Composable function
g

gildor

08/25/2020, 5:50 AM
It means that you call composable function from regular function, which is not allowed (it’s the same as with suspend functions) Do you have any context for this error?
r

Reprator

08/25/2020, 5:55 AM
Copy code
package com.reprator.khatabook_android.ui.login

import android.app.Activity
import android.util.Log
import android.view.inputmethod.InputMethodManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Text
import androidx.compose.foundation.contentColor
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ContextAmbient
import androidx.compose.ui.text.SoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.ui.tooling.preview.Preview
import com.reprator.khatabook_android.ui.snackbarAction
import kotlinx.coroutines.delay

@Preview(showBackground = true)
@Composable
fun DefaultPreviewLogin() {
    LoginPage()
}

@Composable
fun LoginPage() {
    val textValue = remember { mutableStateOf(TextFieldValue()) }

    val submit: () -> Unit = {
        Log.e("Hi", "Hi")

        val text = textValue.value.text
        val error: String? = when {
            text.isEmpty() -> {
                "Please enter phone number"
            }
            text.length < 10 -> {
                "Phone number can't be less than 10"
            }
            else -> null
        }

        if (error.isNullOrBlank()) {

        } else {
            Stack {
                ErrorSnackbar(
                    errorMessage = error,
                    modifier = Modifier.gravity(Alignment.BottomCenter)
                )
            }
        }
    }

    Column(
        modifier = Modifier.fillMaxSize().padding(15.dp),
        verticalArrangement = Arrangement.Center
    ) {
        MaterialTextInputComponent(textValue, submit)
        Spacer(modifier = Modifier.preferredHeight(16.dp))
        MaterialButtonComponent(submit)
    }
}

@Composable
fun MaterialTextInputComponent(textValue: MutableState<TextFieldValue>, buttonClick: () -> Unit) {

    OutlinedTextField(
        value = textValue.value,
        onValueChange = { textFieldValue -> textValue.value = textFieldValue },
        keyboardType = KeyboardType.Phone,
        imeAction = ImeAction.Done,
        label = { Text("Enter Your Phone Number") },
        placeholder = { Text(text = "9041866055") },
        onImeActionPerformed = { imeAction: ImeAction,
                                 softwareKeyboardController: SoftwareKeyboardController? ->
            if (imeAction == ImeAction.Done) {
                softwareKeyboardController?.hideSoftwareKeyboard()
                buttonClick.invoke()
            }
        },
        modifier = Modifier.fillMaxWidth()
    )
}

@Composable
fun MaterialButtonComponent(buttonClick: () -> Unit) {
    val context = ContextAmbient.current

    Button(
        onClick = {
            val imm: InputMethodManager =
                (context as Activity).getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
            imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)

            buttonClick.invoke()
        },
        modifier = Modifier.fillMaxWidth(),
        shape = RoundedCornerShape(16.dp),
        elevation = 5.dp
    ) {
        Text(text = "Submit", modifier = Modifier.padding(6.dp))
    }
}



@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ErrorSnackbar(
    errorMessage: String,
    showError: Boolean = !errorMessage.isNullOrBlank(),
    modifier: Modifier = Modifier,
    onErrorAction: () -> Unit = { },
    onDismiss: () -> Unit = { }
) {
    launchInComposition(showError) {
        delay(timeMillis = 5000L)
        if (showError) {
            onDismiss()
        }
    }

    AnimatedVisibility(
        visible = showError,
        enter = slideInVertically(initialOffsetY = { it }),
        exit = slideOutVertically(targetOffsetY = { it }),
        modifier = modifier
    ) {
        Snackbar(
            modifier = Modifier.padding(16.dp),
            text = { Text(errorMessage) },
            action = {
                TextButton(
                    onClick = {
                        onErrorAction()
                        onDismiss()
                    },
                    contentColor = contentColor()
                ) {
                    Text(
                        text = "Ok",
                        color = MaterialTheme.colors.snackbarAction
                    )
                }
            }
        )
    }
}
Copy code
actually i try to show a snackbar, if input is invalid, but it is throwing me the above mentioned error,
g

gildor

08/25/2020, 5:56 AM
on which like you have this error?
Probably where you declare
submit
lambda
Mark lambda as @Composable too
it will allow you to call composable from it
r

Reprator

08/25/2020, 5:59 AM
line no 49
g

gildor

08/25/2020, 6:00 AM
val submit: () -> Unit
->
val submit: @Composable () -> Unit
same for MaterialTextInputComponent buttonClock
r

Reprator

08/25/2020, 6:09 AM
I am still getting the same error on line 81
Copy code
package com.reprator.khatabook_android.ui.login

import android.app.Activity
import android.util.Log
import android.view.inputmethod.InputMethodManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Text
import androidx.compose.foundation.contentColor
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ContextAmbient
import androidx.compose.ui.text.SoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.ui.tooling.preview.Preview
import com.reprator.khatabook_android.ui.snackbarAction
import kotlinx.coroutines.delay

@Preview(showBackground = true)
@Composable
fun DefaultPreviewLogin() {
    LoginPage()
}

@Composable
fun LoginPage() {
    val textValue = remember { mutableStateOf(TextFieldValue()) }

    val submit: @Composable () -> Unit  = {
        val text = textValue.value.text
        val error: String? = when {
            text.isEmpty() -> {
                "Please enter phone number"
            }
            text.length < 10 -> {
                "Phone number can't be less than 10"
            }
            else -> null
        }
        if (error.isNullOrBlank()) {
        } else {
            Stack {
                ErrorSnackbar(
                    errorMessage = error,
                    modifier = Modifier.gravity(Alignment.BottomCenter)
                )
            }
        }
    }
    Column(
        modifier = Modifier.fillMaxSize().padding(15.dp),
        verticalArrangement = Arrangement.Center
    ) {
        MaterialTextInputComponent(textValue, submit)
        Spacer(modifier = Modifier.preferredHeight(16.dp))
        MaterialButtonComponent(submit)
    }
}
@Composable
fun MaterialTextInputComponent(textValue: MutableState<TextFieldValue>, buttonClick: @Composable () -> Unit) {
    OutlinedTextField(
        value = textValue.value,
        onValueChange = { textFieldValue -> textValue.value = textFieldValue },
        keyboardType = KeyboardType.Phone,
        imeAction = ImeAction.Done,
        label = { Text("Enter Your Phone Number") },
        placeholder = { Text(text = "9041866055") },
        onImeActionPerformed = { imeAction: ImeAction,
                                 softwareKeyboardController: SoftwareKeyboardController? ->
            if (imeAction == ImeAction.Done) {
                softwareKeyboardController?.hideSoftwareKeyboard()
                buttonClick()
            }
        },
        modifier = Modifier.fillMaxWidth()
    )
}
@Composable
fun MaterialButtonComponent(buttonClick: @Composable () -> Unit) {
    val context = ContextAmbient.current
    Button(
        onClick = {
            val imm: InputMethodManager =
                (context as Activity).getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
            imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
            buttonClick()
        },
        modifier = Modifier.fillMaxWidth(),
        shape = RoundedCornerShape(16.dp),
        elevation = 5.dp
    ) {
        Text(text = "Submit", modifier = Modifier.padding(6.dp))
    }
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ErrorSnackbar(
    errorMessage: String,
    showError: Boolean = !errorMessage.isNullOrBlank(),
    modifier: Modifier = Modifier,
    onErrorAction: () -> Unit = { },
    onDismiss: () -> Unit = { }
) {
    launchInComposition(showError) {
        delay(timeMillis = 5000L)
        if (showError) {
            onDismiss()
        }
    }
    AnimatedVisibility(
        visible = showError,
        enter = slideInVertically(initialOffsetY = { it }),
        exit = slideOutVertically(targetOffsetY = { it }),
        modifier = modifier
    ) {
        Snackbar(
            modifier = Modifier.padding(16.dp),
            text = { Text(errorMessage) },
            action = {
                TextButton(
                    onClick = {
                        onErrorAction()
                        onDismiss()
                    },
                    contentColor = contentColor()
                ) {
                    Text(
                        text = "Ok",
                        color = MaterialTheme.colors.snackbarAction
                    )
                }
            }
        )
    }
}
g

gildor

08/25/2020, 6:13 AM
Yeah, I see, it will not work like that
you should show this composition somewhere
it’s not magically apear from nowhere
you should add it your composition tree
so this callback should only change some state, not just call snackbar function
r

Reprator

08/25/2020, 6:15 AM
but how should i show it based on validation?
like in textfield, onValueChange is there, if i put the conditon in conpositon tree, it will continue to show the snackbar, on every input untill the condition is not met
so that's the dilemma for me here
g

gildor

08/25/2020, 6:23 AM
No,you can use snackbar, but it should be somewhere on top level of your screen and render it when some state is changed
r

Reprator

08/25/2020, 6:33 AM
Copy code
package com.reprator.khatabook_android.ui.login

import android.app.Activity
import android.util.Log
import android.view.inputmethod.InputMethodManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Text
import androidx.compose.foundation.contentColor
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ContextAmbient
import androidx.compose.ui.text.SoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.ui.tooling.preview.Preview
import com.reprator.khatabook_android.ui.snackbarAction
import kotlinx.coroutines.delay

@Preview(showBackground = true)
@Composable
fun DefaultPreviewLogin() {
    LoginPage()
}

@Composable
fun LoginPage() {
    val textValue = remember { mutableStateOf(TextFieldValue()) }

    val submit:  () -> Unit  = {

    }
    Column(
        modifier = Modifier.fillMaxSize().padding(15.dp),
        verticalArrangement = Arrangement.Center
    ) {
        MaterialTextInputComponent(textValue, submit)
        Spacer(modifier = Modifier.preferredHeight(16.dp))
        MaterialButtonComponent(submit)


        val text = textValue.value.text

        val error: String? = when {
            text.isEmpty() -> {
                "Please enter phone number"
            }
            text.length < 10 -> {
                "Phone number can't be less than 10"
            }
            else -> null
        }

        if(!error.isNullOrEmpty())
            Stack {
                ErrorSnackbar(
                    errorMessage = error,
                    modifier = Modifier.gravity(Alignment.BottomCenter)
                )
            }
    }
}

@Composable
fun MaterialTextInputComponent(textValue: MutableState<TextFieldValue>, buttonClick: () -> Unit) {
    OutlinedTextField(
        value = textValue.value,
        onValueChange = { textFieldValue -> textValue.value = textFieldValue },
        keyboardType = KeyboardType.Phone,
        imeAction = ImeAction.Done,
        label = { Text("Enter Your Phone Number") },
        placeholder = { Text(text = "9041866055") },
        onImeActionPerformed = { imeAction: ImeAction,
                                 softwareKeyboardController: SoftwareKeyboardController? ->
            if (imeAction == ImeAction.Done) {
                softwareKeyboardController?.hideSoftwareKeyboard()
                buttonClick()
            }
        },
        modifier = Modifier.fillMaxWidth()
    )
}
@Composable
fun MaterialButtonComponent(buttonClick:() -> Unit) {
    val context = ContextAmbient.current
    Button(
        onClick = {
            val imm: InputMethodManager =
                (context as Activity).getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
            imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
            buttonClick()
        },
        modifier = Modifier.fillMaxWidth(),
        shape = RoundedCornerShape(16.dp),
        elevation = 5.dp
    ) {
        Text(text = "Submit", modifier = Modifier.padding(6.dp))
    }
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ErrorSnackbar(
    errorMessage: String,
    showError: Boolean = !errorMessage.isNullOrBlank(),
    modifier: Modifier = Modifier,
    onErrorAction: () -> Unit = { },
    onDismiss: () -> Unit = { }
) {
    launchInComposition(showError) {
        delay(timeMillis = 5000L)
        if (showError) {
            onDismiss()
        }
    }
    AnimatedVisibility(
        visible = showError,
        enter = slideInVertically(initialOffsetY = { it }),
        exit = slideOutVertically(targetOffsetY = { it }),
        modifier = modifier
    ) {
        Snackbar(
            modifier = Modifier.padding(16.dp),
            text = { Text(errorMessage) },
            action = {
                TextButton(
                    onClick = {
                        onErrorAction()
                        onDismiss()
                    },
                    contentColor = contentColor()
                ) {
                    Text(
                        text = "Ok",
                        color = MaterialTheme.colors.snackbarAction
                    )
                }
            }
        )
    }
}
the snackbar will continue to show, until the error is not satisfied
how to get rid of that snackbar
g

gildor

08/25/2020, 6:37 AM
your
error
should be remember { mutableStateOf() }
and add onDismiss = { error.value = null }
it will cause recomposition and dismiss snackbar
you still need some additional error state
now you determine it only by checking field content
so there is no way to dismiss snackbar
r

Reprator

08/25/2020, 6:56 AM
Now my code is working, but love to know your suggestion regarding review
Copy code
package com.reprator.khatabook_android.ui.login

import android.app.Activity
import android.view.inputmethod.InputMethodManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Text
import androidx.compose.foundation.contentColor
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ContextAmbient
import androidx.compose.ui.text.SoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.ui.tooling.preview.Preview
import com.reprator.khatabook_android.ui.snackbarAction
import kotlinx.coroutines.delay

@Preview(showBackground = true)
@Composable
fun DefaultPreviewLogin() {
    LoginPage()
}

@Composable
fun LoginPage() {
    val textValue = remember { mutableStateOf(TextFieldValue()) }
    var error by remember { mutableStateOf(String()) }

    val submit: () -> Unit = {
        val text = textValue.value.text

        error = when {
            text.isEmpty() -> {
                "Please enter phone number"
            }
            text.length < 10 -> {
                "Phone number can't be less than 10"
            }
            else -> ""
        }
    }

    val onPopupDismissed = { error = "" }

    Column(
        modifier = Modifier.fillMaxSize().padding(15.dp),
        verticalArrangement = Arrangement.Center
    ) {
        MaterialTextInputComponent(textValue, submit)
        Spacer(modifier = Modifier.preferredHeight(16.dp))
        MaterialButtonComponent(submit)

        Stack {
            ErrorSnackbar(
                errorMessage = error,
                modifier = Modifier.gravity(Alignment.BottomCenter),
                onDismiss = onPopupDismissed
            )
        }
    }
}

@Composable
fun MaterialTextInputComponent(textValue: MutableState<TextFieldValue>, buttonClick: () -> Unit) {
    OutlinedTextField(
        value = textValue.value,
        onValueChange = { textFieldValue -> textValue.value = textFieldValue },
        keyboardType = KeyboardType.Phone,
        imeAction = ImeAction.Done,
        label = { Text("Enter Your Phone Number") },
        placeholder = { Text(text = "9041866055") },
        onImeActionPerformed = { imeAction: ImeAction,
                                 softwareKeyboardController: SoftwareKeyboardController? ->
            if (imeAction == ImeAction.Done) {
                softwareKeyboardController?.hideSoftwareKeyboard()
                buttonClick()
            }
        },
        modifier = Modifier.fillMaxWidth()
    )
}

@Composable
fun MaterialButtonComponent(buttonClick: () -> Unit) {
    val context = ContextAmbient.current
    Button(
        onClick = {
            val imm: InputMethodManager =
                (context as Activity).getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
            imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
            buttonClick()
        },
        modifier = Modifier.fillMaxWidth(),
        shape = RoundedCornerShape(16.dp),
        elevation = 5.dp
    ) {
        Text(text = "Submit", modifier = Modifier.padding(6.dp))
    }
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ErrorSnackbar(
    errorMessage: String,
    showError: Boolean = errorMessage.isNotBlank(),
    modifier: Modifier = Modifier,
    onErrorAction: () -> Unit = { },
    onDismiss: () -> Unit = { }
) {
    launchInComposition(showError) {
        delay(timeMillis = 5000L)
        if (showError) {
            onDismiss()
        }
    }
    AnimatedVisibility(
        visible = showError,
        enter = slideInVertically(initialOffsetY = { it }),
        exit = slideOutVertically(targetOffsetY = { it }),
        modifier = modifier
    ) {
        Snackbar(
            modifier = Modifier.padding(16.dp),
            text = { Text(errorMessage) },
            action = {
                TextButton(
                    onClick = {
                        onErrorAction()
                        onDismiss()
                    },
                    contentColor = contentColor()
                ) {
                    Text(
                        text = "Ok",
                        color = MaterialTheme.colors.snackbarAction
                    )
                }
            }
        )
    }
}
g

gildor

08/25/2020, 7:00 AM
Huh, String() is not the most simple way to create an empty string 😄
In general there is more convinient way to manage Snackbar, is use Scaffold
r

Reprator

08/25/2020, 7:04 AM
thanks for your assistance @gildor
g

gildor

08/25/2020, 7:05 AM
Code can be improved by separating business logic (validation) from UI, but it’s in general correct way to use snackbar (tho I would check Scafoold, see snackbarHost param
r

Reprator

08/25/2020, 7:08 AM
i am quite new to compose, it will take time for me to go with clean code, after reading some open source code, i will try to separate it out
but all in thanks
but love to know how to separate the logic
any hint
g

gildor

08/25/2020, 7:39 AM
It may looks something like this with Skaffold: https://gist.github.com/gildor/82ec960cc0c5873453f024870495eab3
r

Reprator

08/25/2020, 8:02 AM
@gildor Awesome code
g

gildor

08/25/2020, 8:05 AM
Not an expert, never used scaffold for snackbar, maybe I did something wrong (should scaffold state be abstracted? Is that correct way to use coroutines scope for this case), but at least it works
2 Views