Suppose I have four Button or Composable in a row, I want to select one and deselect the other, not like the radio button but behave exactly like that.
#Composable
fun ButtonSet(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.padding(start = 60.dp, end = 60.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
val button1 = remember { mutableStateOf(false) }
val button2 = remember { mutableStateOf(false) }
val button3 = remember { mutableStateOf(false) }
val button4 = remember { mutableStateOf(false) }
val buttonList = listOf<Any>(button1, button2, button3, button4)
val activeButton = remember {
mutableStateOf(true)
}
if (button1.value || button2.value || button3.value || button4.value) {
activeButton.value = false
} else if (button1.value) {
button1.value = !button1.value
} else if (button2.value) {
button2.value = !button2.value
} else if (button3.value) {
button3.value = !button3.value
} else if (button4.value) {
button4.value = !button4.value
}
GoalkeeperButton(button1.value)
GoalkeeperButton(button2.value)
GoalkeeperButton(button3.value)
GoalkeeperButton(button4.value)
}
}
#Composable
fun GoalkeeperButton(
active: Boolean = false,
#SuppressLint("ModifierParameter") modifier: Modifier = Modifier,
) {
val buttonState = remember {
mutableStateOf(active)
}
if (!buttonState.value) {
ButtonUnActive() {}
} else {
ButtonActive()
}
RadioButton(selected =, onClick = { /*TODO*/ })
}
#Composable
fun ButtonActive(active: Boolean = true) {
val button = painterResource(id = R.drawable.bttn_pressed)
val gloves = painterResource(id = R.drawable.blue_hands)
val buttonState = remember {
mutableStateOf(active)
}
Column(
modifier = Modifier
.wrapContentWidth()
.height(130.dp)
) {
Image(
painter = gloves,
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.size(width = 135.dp, height = 70.dp)
)
Image(
painter = button,
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.clickable(onClick = {})
.size(width = 107.dp, height = 65.dp)
)
}
}
#Composable
fun ButtonUnActive(
state: Boolean = false,
onclick: () -> Unit,
) {
val button = painterResource(id = R.drawable.bttn)
var unActiveState by remember {
mutableStateOf(state)
}
Image(
painter = button,
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.clickable(
onClick = {
onclick()
},
)
.size(width = 107.dp, height = 65.dp)
)
}
This is what I tried, but I know it's very bad code, please suggest a better and best way.
Related
I am trying to load a list of object in a column using jetpack compose.
The problem is the image is making screen lag.
Here's my code - single list item composable function.
fun SoulProfilePreviewSingleItem(
soul: Souls,
soulWinningViewModel: SoulWinningViewModel,
onClick: () -> Unit
) {
val lazyListState = rememberLazyListState()
/* val localImage = if (soul.localImage.isNotBlank())
rememberImagePainter(getBase64StringToImage(soul.localImage)) else null*/
LaunchedEffect(key1 = soulWinningViewModel.nurtureVsFollowUpTabIndex.value) {
lazyListState.animateScrollToItem(0)
// if (soul.localImage.isNotBlank()){
// }
}
val isManaging = soulWinningViewModel.isManagingList.value
var isDeleted by remember {
mutableStateOf(false)
}
val setVisibility: (Boolean) -> Unit = {
isDeleted = it
}
AnimatedVisibility(
visible = isDeleted.not() &&
if (soulWinningViewModel.userIsSearchingFor.value.isNotEmpty() && isManaging.not())
soul.name.contains(soulWinningViewModel.userIsSearchingFor.value, true) ||
soul.followUpActions.toString()
.contains(soulWinningViewModel.userIsSearchingFor.value, true) else
true
) {
with(soul) {
Column {
Box(
modifier = Modifier
.height(60.dp)
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth(),
contentAlignment = Alignment.BottomEnd
) {
Row(
Modifier
.fillMaxSize(),//.clickable { onClick() },
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.Top,
modifier = Modifier
.padding(start = brc_DP_small)
.padding(bottom = 6.dp, top = 6.dp)
) {
/*if (soul.localImage.isNotBlank()) {
GlideImage(
model = ,
// Crop, Fit, Inside, FillHeight, FillWidth, None
contentScale = ContentScale.Crop,
// shows an image with a circular revealed animation.
)
Log.e("BITMAP","onResourceReady test")
val image = loadPicture(url = soul.localImage, defaultImage = R.drawable.ic_brc_logo).value
image?.let { img->
Image(
// Can crash this
bitmap =img.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(50))
.size(30.dp),
contentScale = ContentScale.Crop
)
}
}*/
if (soul.localImage.isNotBlank()) {
// val imageLoader = rememberImagePainter(getBase64StringToImage(soul.localImage))
Image(
// painter = imageLoader
// Can crash this
bitmap = (
getBase64StringToImage(soul.localImage).asImageBitmap()
),
contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(50))
.size(30.dp),
contentScale = ContentScale.Crop
)
} else {
Image(
// Can crash this
painterResource(id = R.drawable.ic_brc_logo),
contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(50))
.size(30.dp),
contentScale = ContentScale.Crop
)
}
}
Column(
modifier = Modifier
.padding(top = brc_DP_small)
.weight(1f)
.fillMaxWidth()
.fillMaxHeight()
.padding(start = brc_DP_smaller),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier
.weight(1f)
.fillMaxSize()
) {
// This is the place where signle soul is getting showed......
Text(
text = name,
style = if (isManaging.not()) MaterialTheme.typography.labelSmall.copy(
fontWeight = FontWeight(500),
letterSpacing = 0.7.sp
)
else MaterialTheme.typography.labelSmall
.copy(
fontSize = 10.sp
),
modifier = Modifier,
maxLines = 1,
)
if (isManaging.not()) {
Text(
text = SimpleDateFormat(
"dd-MMM-yyyy",
Locale.UK
).format(
createdOn
),
style = MaterialTheme.typography.bodySmall.copy(fontSize = 9.sp),
color = Color.Gray,
modifier = Modifier
)
}
}
if (isBrcMember) {
Text(
text = "BRC Member",
style = MaterialTheme.typography.labelSmall.copy(
fontSize = 7.sp,
fontStyle = FontStyle.Italic
),
color = brc_blue_color,
modifier = Modifier.padding(bottom = brc_DP_smallest)
)
}
}
}
var externClick by remember {
mutableStateOf(false)
}
Column(modifier = Modifier.padding(brc_DP_small)) {
AddSoulFollowUpActionButton(
soul = soul,
soulWinningViewModel = soulWinningViewModel,
visible = soulWinningViewModel.nurtureVsFollowUpTabIndex.value == 1,
externClick = externClick,
onFinished = { externClick = false }
) {
setVisibility(it)
}
}
//TODO:Check Click area ...
Row(modifier = Modifier.fillMaxSize()) {
Spacer(
modifier = Modifier
.fillMaxSize()
.weight(1.4f)
.clickable(
interactionSource = MutableInteractionSource(),
indication = null
) { onClick() })
if (soulWinningViewModel.nurtureVsFollowUpTabIndex.value == 1 || isManaging) {
Spacer(
modifier = Modifier
.fillMaxSize()
.weight(0.6f)
.clickable(
interactionSource = MutableInteractionSource(),
indication = null
) { externClick = !externClick })
}
}
}
Spacer(
modifier = Modifier
.padding(start = 28.dp)
.fillMaxWidth() //.then(if (isManaging.not()) Modifier.width(150.dp) else Modifier.fillMaxWidth())
.height(0.3.dp)
.background(Color.LightGray)
)
}
}
}
}
caller of list composable function.
forEach {
SoulProfilePreviewSingleItem(
soul = it,
soulWinningViewModel
) {
soulWinningViewModel.currentSoul.value = it
navController.navigate(route = SoulWinningRoutes.PROFILE)
}
}
Whenever I am trying to show Image() of my soul object I am getting glitches and my screens takes more time to load itself.
I am storing image as string in local room database and trying to retrieve it back converting it to bitmapImage.
Code for conversation.
fun getBase64StringToImage(base64String: String): Bitmap {
val decodedString: ByteArray = Base64.decode(base64String, Base64.DEFAULT)
val decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size)
return decodedByte
// this is taking a lot of time and making my Ui performance dull.
}
Is there any way I can achieve this functionality with good performance. Like loading Image in separate scope or anything else.
I have already tried using rememberImagePainter and Glide Coil. rememberImagePainter making my performance worst.
Please feel free to give me suggestions, I am still in learning stage.
Thanks in advance.
I'm am making a note app using kotlin jetpack compose. Currently, I'm working on an update feature. I'm trying to update a note in ROOM Database but it doesn't update. I already read and follow some tutorials from documentation and youtube, however, I still haven't solved it.
Here is my code:
Data
#RequiresApi(Build.VERSION_CODES.O)
#Entity(tableName = "KeepNotesTable")
data class NoteData (
#PrimaryKey
val id: String = UUID.randomUUID().toString(),
#ColumnInfo(name = "noteTitle")
var title: String,
#ColumnInfo(name = "noteDescription")
var description: String,
#ColumnInfo(name = "noteDate")
val date: Date = Date.from(Instant.now())
)
DAO
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun updateNote(note: NoteData)
Repository
suspend fun updateNote(note: NoteData) = noteDatabaseDao.updateNote(note)
ViewModel
fun updateNote(note: NoteData) = viewModelScope.launch { repository.updateNote(note) }
Navigation
#RequiresApi(Build.VERSION_CODES.O)
#Composable
fun ScreenNavigation(noteViewModel: NoteViewModel = viewModel()) {
val noteList = noteViewModel.noteList.collectAsState().value
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.MainScreen.name) {
composable(Screen.MainScreen.name){
MainScreen(navController = navController, noteList = noteList, removeNote = { noteViewModel.removeNote(it) })}
composable(Screen.AddNoteScreen.name){
AddNoteScreen(navController = navController, addNote = { noteViewModel.addNote(it) })
}
composable(Screen.UpdateNoteScreen.name + "/{noteId}",
arguments = listOf(navArgument(name = "noteId") {type = NavType.StringType})
) { backStackEntry ->
UpdateNoteScreen(
noteList = noteList,
navController = navController,
updateNote = { noteViewModel.updateNote(it) },
NoteId = backStackEntry.arguments?.getString("noteId")
)
}
}
}
UpdateNoteScreen
#RequiresApi(Build.VERSION_CODES.O)
#Composable
fun UpdateNoteScreen(
noteList: List<NoteData>,
navController: NavController,
updateNote: (NoteData) -> Unit,
NoteId: String?,
) {
val fetchNote = noteList.filter { note ->
note.id == NoteId
}
val note = fetchNote.first()
var noteTitle = note.title
var noteDescription = note.description
var newTitle = ""
var newDescription = ""
var title by remember {
mutableStateOf(noteTitle + newTitle)
}
var description by remember {
mutableStateOf(noteDescription + newDescription)
}
val context = LocalContext.current
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.primaryVariant) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Card(
contentColor = MaterialTheme.colors.primary,
backgroundColor = MaterialTheme.colors.background,
elevation = 8.dp,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 20.dp, bottom = 20.dp)) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(start = 15.dp, end = 15.dp, top = 10.dp, bottom = 40.dp)) {
InputText(
modifier = Modifier.fillMaxWidth(),
text = title,
label = "",
onTextChange = {
newTitle = it
title = newTitle
}
)
Spacer(modifier = Modifier.height(10.dp))
InputText(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.5f),
text = description,
label = "",
maxLine = 1000,
imeAction = ImeAction.None,
onTextChange = {
newDescription = it
description = newDescription
}
)
}
}
Row() {
DiscardButton(text = "Cancel", onClick = {
navController.navigate(route = Screen.MainScreen.name)
})
Spacer(modifier = Modifier.width(10.dp))
SaveButton(text = "Update", onClick = {
if (title.isNotEmpty() && description.isNotEmpty()) {
updateNote(NoteData(title = title, description = description))
navController.navigate(route = Screen.MainScreen.name)
Toast.makeText(context, "Successfully Updated", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Fill the Title and Description", Toast.LENGTH_SHORT).show()
}
})
}
}
}
}
I reviewed my code again and noticed that an id parameter is needed to pass in the updateNote() to find and update the specific note. Therefore,
updateNoteScreen
updateNote(NoteData(title = title, description = description)
should be
updateNote(NoteData(id = note.id, title = title, description = description)
I need to have a component that scrolls vertically and horizontally.
Here is what I did so far:
#Composable
fun Screen() {
val scope = rememberCoroutineScope()
val scrollOffset = remember {
mutableStateOf(value = 0F)
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(10) {
SimpleRow(
onScrollOffsetChange = {
scrollOffset.value = it
}
)
}
}
}
#Composable
private fun SimpleRow(
onScrollOffsetChange: (Float) -> Unit,
) {
val lazyRowState = rememberLazyListState()
LazyRow(
modifier = Modifier.fillMaxWidth(),
state = lazyRowState
) {
item {
Text(
text = "firstText"
)
}
for (i in 1..30) {
item {
Text(text = "qwerty")
}
}
}
}
When anyone of these SimpleRow is scrolled I want to scroll all of the SimpleRows together.
And I do not know how many SimpleRows I have because they came from our server.
Is there any kind of composable or trick that can do it for me?
You can use multiple rows inside a column that scrolls horizontally and vertically, like this:
#Composable
fun MainContent() {
val scrollStateHorizontal = rememberScrollState()
val scrollStateVertical = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollStateVertical)
.horizontalScroll(state = scrollStateHorizontal)
) {
repeat(40){ c ->
Row {
repeat(40){ r ->
Item("col: $c | row: $r")
}
}
}
}
}
#Composable
fun Item(text: String) {
Text(
modifier = Modifier
.padding(5.dp)
.background(Color.Gray),
text = text,
color = Color.White
)
}
I'm wanting the menu to load the data that comes from the database. This is being static manually, does anyone have any idea how I can do this? Here's my current structure. Anything if you need more code details I can show you
My method:
#Composable
private fun DrawerContent(
scope: CoroutineScope,
scaffoldState:ScaffoldState,
navController: NavController
){
val items = listOf(
Screen.Entry1,
Screen.Entry2,
Screen.Entry3
)
Column(modifier= Modifier
.background(colorResource(id = R.color.pastel_green))
.fillMaxWidth()
.fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally) {
Text(
"Menu", style = MaterialTheme.typography.h5,
modifier = Modifier.padding(bottom = 20.dp,top=20.dp),
color = colorResource(id = R.color.marron)
)
val current by navController.currentBackStackEntryAsState()
val currentRoute = current?.destination
items.forEach { screen ->
var selected = currentRoute?.hierarchy?.any { it.route == screen.route } == true
val selectedColor = if (selected) colorResource(id = R.color.cinza) else Color.Transparent
val colorfont = if (selected) colorResource(id = R.color.white) else colorResource(id = R.color.cinza)
Row(modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.background(selectedColor)
.clickable {
selected =
currentRoute?.hierarchy?.any { it.route == screen.route } == true
scope.launch { scaffoldState.drawerState.close() }
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}) {
Text(stringResource(screen.resourceString),fontSize = 20.sp,
color= colorfont)
}
}
}
}
Class Screen:
sealed class Screen(val route: String, #StringRes val resourceString: Int,#IdRes val resourceId:Int) {
object Entry1 : Screen("entry1", R.string.entry1, R.id.entry1)
object Entry2 : Screen("entry2", R.string.entry2, R.id.entry2)
object Entry3 : Screen("entry3", R.string.entry3, R.id.entry3)
}
solved
My Code:
NavHost(
navController = navController,
// 1st change: Set startDestination to the exact string of route
startDestination = "entry${items[0].category_id}/{$ARG_ID}", // NOT "dynamic/1", provide arguments via defaultValue
) {
items.forEach { screen ->
composable(
route = "entry${screen.category_id}/{$ARG_ID}",
// 2nd change: Set startDestination argument via defaultValue
arguments = listOf(navArgument(ARG_ID) {
type = NavType.IntType; defaultValue =
screen.category_id
}),
) {
entryAbstract.Entry1(id = it.arguments?.getInt(ARG_ID))
}
}
}
companion object
{
const val ARG_ID = "id"
}
My method composable
#Composable
private fun DrawerContent(scope: CoroutineScope,scaffoldState:ScaffoldState
,navController: NavController
) {
Column(
modifier = Modifier
.background(colorResource(id = R.color.pastel_green))
.fillMaxWidth()
.fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Menu", style = MaterialTheme.typography.h5,
modifier = Modifier.padding(bottom = 20.dp, top = 20.dp),
color = colorResource(id = R.color.marron)
)
val current by navController.currentBackStackEntryAsState()
val currentRoute = current?.destination
items.forEach { screen ->
var selected = currentRoute?.hierarchy?.any { it.route == "entry${screen.category_id}/{$ARG_ID}" } == true
val selectedColor = if (selected) colorResource(id = R.color.cinza) else Color.Transparent
val colorfont = if (selected) colorResource(id = R.color.white) else colorResource(id = R.color.cinza)
Row(modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.background(selectedColor)
.clickable {
selected =
currentRoute?.hierarchy?.any { it.route == "entry${screen.category_id}/{$ARG_ID}" } == true
scope.launch { scaffoldState.drawerState.close() }
navController.navigate("entry${screen.category_id}/${screen.category_id}") {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}) {
Text(screen.category_name, fontSize = 20.sp, color = colorfont)
}
}
}
}
A colleague of mine has written the following code:
Viewmodel:
class CallMeBackViewModel(
private val ewayControllerUseCase: EwayControllerUseCase,
private val accountControllerUseCase: AccountControllerUseCase
) : ViewModel() {
val accountDetails = MutableLiveData<AccountDetailsResponse>()
val contactForm = CallMeBackForm()
init {
val accountId = App.instance.appContext.getPreferenceThroughKeystore(PreferenceKeys.ACCOUNT_ID.key)
contactForm.timeframe = "09:00 - 11:00"
if (accountId != "null") {
getAccountDetails(accountId)
}
}
private fun getAccountDetails(accountId: String) {
viewModelScope.launch(Dispatchers.IO) {
when (val response = accountControllerUseCase.getAccountDetails(accountId)) {
is ResponseResult.Success -> {
accountDetails.postValue(response.data!!)
fillContactFormByAccount(response.data!!)
}
is ResponseResult.Error -> {
Log.d("GET_ACCOUNT_DETAILS_ERROR", "Network Call Failed With Exception: " + response.exception.message)
}
}
}
}
private fun fillContactFormByAccount(accountDetailsResponse: AccountDetailsResponse) {
contactForm.firstName = accountDetailsResponse.firstName!!
contactForm.lastName = accountDetailsResponse.lastName!!
contactForm.phoneNumber = accountDetailsResponse.phone1!!
contactForm.accountId = accountDetailsResponse.accountId!!
}
#ExperimentalFoundationApi
#ExperimentalMaterialApi
fun submitCallbackForm(activity: MainActivity) {
viewModelScope.launch(Dispatchers.IO) {
when (val response = ewayControllerUseCase.submitCallbackForm(contactForm)) {
is ResponseResult.Success -> {
onSuccessfulSubmission(activity)
}
is ResponseResult.Error -> {
Log.d("CALLBACK_FORM_SUBMISSION_ERROR", "Network Call Failed With Exception: " + response.exception.message)
}
}
}
}
#ExperimentalFoundationApi
#ExperimentalMaterialApi
private suspend fun onSuccessfulSubmission(activity: MainActivity) {
withContext(Dispatchers.Main) {
activity.onBackPressed()
}
}
}
He basically fetches some form data and when received, updates the accountDetails live data with them. Now in the composable screen he observes this field as a state like so :
Screen:
#ExperimentalFoundationApi
#ExperimentalMaterialApi
#Composable
fun ContactCallMeBack() {
val viewModel: CallMeBackViewModel = remember { getKoin().get() }
val accountDetails by viewModel.accountDetails.observeAsState(null)
val canProceed = remember { mutableStateOf(false) }
val textChangedToggle = remember { mutableStateOf(false) }
val activity = LocalContext.current as MainActivity
LaunchedEffect(textChangedToggle.value) {
canProceed.value = canProceed(viewModel.contactForm)
}
Column(modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
TopBarPadding(true)
Spacer(modifier = Modifier.padding(24.dp))
if (accountDetails != null) {
Form(accountDetails, viewModel, textChangedToggle)
}
else {
Form(accountDetails, viewModel, textChangedToggle)
}
Spacer(modifier = Modifier.padding(42.fixedDp()))
MyButton(text = "Καλέστε με",
buttonType = if (canProceed.value) {
MyButtonType.PRIMARY
} else {
MyButtonType.DISABLED
},
onClick = { viewModel.submitCallbackForm(activity) })
Spacer(modifier = Modifier.padding(49.fixedDp()))
}
}
#Composable
private fun Form(
accountDetails: AccountDetailsResponse?,
viewModel: CallMeBackViewModel,
textChangedToggle: MutableState<Boolean>
) {
Column(modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(13.dp))
.background(Color.White)
.padding(start = 16.dp, end = 16.dp, top = 22.dp, bottom = 24.dp)
) {
InputField(label = "Κωδικός Συνδρομητή",
initialValue = accountDetails?.accountId ?: "",
onTextChangeCallback = {
viewModel.contactForm.accountId = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Όνομα",
initialValue = accountDetails?.firstName ?: "",
onTextChangeCallback = {
viewModel.contactForm.firstName = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Επώνυμο",
initialValue = accountDetails?.lastName ?: "",
onTextChangeCallback = {
viewModel.contactForm.lastName = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Τηλέφωνο",
initialValue = accountDetails?.phone1 ?: "",
onTextChangeCallback = {
viewModel.contactForm.phoneNumber = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
MyDropdown(label = "Διαθεσιμότητα", list = mapOf(
1 to "09:00 - 11:00",
2 to "11:00 - 13:00",
3 to "13:00 - 15:00",
4 to "15:00 - 17:00"
),
defaultSelected = 1,
disableSelectionReset = true,
onSelectionChangeCallbackValue = { viewModel.contactForm.timeframe = it }
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Μήνυμα", singleLine = false,
onTextChangeCallback = {
viewModel.contactForm.reason = it
triggerCheck(textChangedToggle)
}
)
}
}
private fun canProceed(form: CallMeBackForm): Boolean {
return (form.firstName != "" &&
form.lastName != "" &&
form.phoneNumber != "" &&
form.reason != "")
}
private fun triggerCheck(state: MutableState<Boolean>) {
state.value = !state.value
}
InputField composable:
#Composable
fun InputField(
label: String,
enabled: Boolean = true,
isPassword: Boolean = false,
infoButton: (() -> Unit)? = null,
placeholder: String = LocalContext.current.getString(R.string.input_placeholder),
canReveal: Boolean = false,
phonePrefix: Boolean = false,
singleLine: Boolean = true,
optional: Boolean = false,
initialValue: String = "",
onTextChangeCallback: (String) -> Unit = { },
numbersOnly: Boolean = false
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 10.fixedDp())
) {
Text(
text = label,
fontSize = 16.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(end = 10.fixedDp())
)
if (optional) {
Text(
text = LocalContext.current.getString(R.string.optional),
fontSize = 16.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(end = 10.fixedDp()),
color = colorResource(id = R.color.input_field_optional_text)
)
}
if (infoButton != null) {
InfoButton(
buttonColor = colorResource(id = R.color.Orange_100),
onClick = infoButton
)
}
}
Spacer(modifier = Modifier.padding(10.fixedDp()))
val textFieldValue = remember { mutableStateOf(TextFieldValue(initialValue)) }
val textFieldPasswordMode = remember { mutableStateOf(isPassword) }
val modifier = if (singleLine) {
Modifier
.clip(RoundedCornerShape(6.dp))
.fillMaxWidth()
.background(if (enabled) Color.White else colorResource(id = R.color.Grey_10))
} else {
Modifier
.clip(RoundedCornerShape(6.dp))
.fillMaxWidth()
.requiredHeight(100.dp)
.background(if (enabled) Color.White else colorResource(id = R.color.Grey_10))
}
var isTextFieldFocused by remember { mutableStateOf(false) }
ShadowWrapper(
cardElevation = 1.dp,
border = BorderStroke(
1.dp,
if (isTextFieldFocused && enabled) colorResource(id = R.color.Orange_100)
else colorResource(id = R.color.my_card_border)
),
shadowShapeRadius = 6.dp,
shadowElevation = 1.dp
) {
TextField(
value = textFieldValue.value,
onValueChange = {
textFieldValue.value = it
onTextChangeCallback.invoke(it.text)
},
modifier = modifier
.onFocusChanged { isTextFieldFocused = it.hasFocus },
keyboardOptions = if (numbersOnly) {
KeyboardOptions(keyboardType = KeyboardType.Phone)
} else {
KeyboardOptions(keyboardType = KeyboardType.Text)
},
enabled = enabled,
colors = TextFieldDefaults.textFieldColors(
disabledTextColor = colorResource(id = R.color.Grey_60),
focusedIndicatorColor = Color.Transparent,
backgroundColor = if (enabled) colorResource(id = R.color.white) else colorResource(
id = R.color.Grey_10
),
textColor = colorResource(id = R.color.black),
unfocusedIndicatorColor = colorResource(id = R.color.white),
disabledIndicatorColor = colorResource(id = R.color.white),
cursorColor = colorResource(id = R.color.black)
),
singleLine = singleLine,
placeholder = {
Text(
text = placeholder,
fontStyle = FontStyle.Italic,
fontSize = 16.sp,
lineHeight = 20.sp,
overflow = TextOverflow.Visible
)
},
readOnly = !enabled,
visualTransformation = if (textFieldPasswordMode.value) PasswordVisualTransformation() else VisualTransformation.None,
leadingIcon = if (phonePrefix) {
{
PhonePrefix(R.drawable.greek_flag, "+30")
}
} else null,
trailingIcon = if (canReveal) {
{
RevealingEye(state = textFieldPasswordMode)
}
} else null,
shape = RoundedCornerShape(6.dp),
)
}
}
}
I noticed the seemingly useless if statement around the form, but it seems to be the only thing forcing the recomposition of the Form composable. I thought that when a new accountDetails instance is posted inside an observable state, that this would trigger any composables that depend on it to recompose. What am I missing?