How to sort this issue, @Composable invocations ca...
# compose
r
How to sort this issue, @Composable invocations can only happen from the context of a @Composable function
g
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
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
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
line no 49
g
val submit: () -> Unit
->
val submit: @Composable () -> Unit
same for MaterialTextInputComponent buttonClock
r
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
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
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
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
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
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
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
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
thanks for your assistance @gildor
g
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
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
It may looks something like this with Skaffold: https://gist.github.com/gildor/82ec960cc0c5873453f024870495eab3
r
@gildor Awesome code
g
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