Multiple ModalBottomSheet in Compose not updated when state changes - android

I have implemented 2 bottom sheets in the ModalBottomSheetLayout, both bottom sheets has a list of item checkable with checkbox.
The state of the screen is managed by the viewModel and when the selection changes is invoked a function that copies the state with the new value of the selected text.
When the bottom sheet opens the selection is correct but when I click to change the selection, the bottomsheet is not recomposed and the selection does not change, but in the main screen the state change is correctly read and the value is updated.
Here my code:
MainScreen:
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
#ExperimentalMaterialApi
#Composable
fun MainScreen(
viewModel: MainViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
) {
val screenState = viewModel.screenState
val scope = rememberCoroutineScope()
val bottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden
)
var bottomSheetContent: (#Composable () -> Unit)? by remember {
mutableStateOf(null)
}
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetContent = {
Box(
modifier = Modifier.defaultMinSize(minHeight = 1.dp)
) {
bottomSheetContent?.let { it() }
}
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(text = "First BottomSheet", style = MaterialTheme.typography.h6)
Text(
text = "Selected: ${screenState.selectedTextFromFirstBottomSheet}",
Modifier.padding(16.dp)
)
Button(onClick = {
bottomSheetContent = {
FirstBottomSheet(
selectedText = screenState.selectedTextFromFirstBottomSheet,
onSelected = { text ->
viewModel.onEvent(
MainScreenEvent.OnFirstBottomSheetSelectedTextChanged(text)
)
},
textList = screenState.firstBottomSheetTextList
)
}
scope.launch {
bottomSheetState.show()
}
}, modifier = Modifier.padding(16.dp)) {
Text(text = " Open First BottomSheet")
}
Text(text = "Second BottomSheet", style = MaterialTheme.typography.h6)
Text(
text = "Selected: ${screenState.selectedTextFromSecondBottomSheet}",
Modifier.padding(16.dp)
)
Button(
onClick = {
bottomSheetContent = {
SecondBottomSheet(
selectedText = screenState.selectedTextFromSecondBottomSheet,
onSelected = { text ->
viewModel.onEvent(
MainScreenEvent.OnSecondBottomSheetSelectedTextChanged(text)
)
},
textList = screenState.secondBottomSheetTextList
)
}
scope.launch {
bottomSheetState.show()
}
}, modifier = Modifier
.padding(16.dp)
) {
Text(text = " Open Second BottomSheet")
}
}
}
}
ViewModel:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel(){
var screenState by mutableStateOf(MainScreenState())
fun onEvent(event: MainScreenEvent){
when(event){
is MainScreenEvent.OnFirstBottomSheetSelectedTextChanged -> {
screenState = screenState.copy(
selectedTextFromFirstBottomSheet = event.text
)
}
is MainScreenEvent.OnSecondBottomSheetSelectedTextChanged -> {
screenState = screenState.copy(
selectedTextFromSecondBottomSheet = event.text
)
}
}
}
}
ScreenState
data class MainScreenState(
val selectedTextFromFirstBottomSheet: String = "First Text b1",
val selectedTextFromSecondBottomSheet: String = "Third Text b2",
val firstBottomSheetTextList: List<String> = listOf(
"First Text b1",
"Second Text b1",
"Third Text b1",
"Fourth Text b1",
"Five Text b1"
),
val secondBottomSheetTextList: List<String> = listOf(
"First Text b2",
"Second Text b2",
"Third Text b2",
"Fourth Text b2",
"Five Text b2"
)
)
Screen Event
sealed class MainScreenEvent(){
data class OnFirstBottomSheetSelectedTextChanged(val text: String): MainScreenEvent()
data class OnSecondBottomSheetSelectedTextChanged(val text: String): MainScreenEvent()
}
First Bottom Sheet
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
#Composable
fun FirstBottomSheet(
selectedText: String,
textList: List<String>,
onSelected: (text: String) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
textList.forEach { text ->
Row(modifier = Modifier
.fillMaxWidth()
.toggleable(
value = selectedText == text,
role = Role.Checkbox,
onValueChange = { isSelected ->
if (isSelected) {
onSelected(text)
}
}
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = text, modifier = Modifier.weight(1f))
Checkbox(checked = selectedText == text, onCheckedChange = null)
}
}
}
}
Second Bottom Sheet
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
#Composable
fun SecondBottomSheet(
selectedText: String,
textList: List<String>,
onSelected: (text: String) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
textList.forEach { text ->
Row(modifier = Modifier
.fillMaxWidth()
.toggleable(
value = selectedText == text,
role = Role.Checkbox,
onValueChange = { isSelected ->
if (isSelected) {
onSelected(text)
}
}
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically) {
Text(text = text, modifier = Modifier.weight(1f))
Checkbox(checked = selectedText == text, onCheckedChange = null)
}
}
}
}
Thanks for your help!

I copied and pasted your code. The only changes I made were:
Remove this line from MainScreen
val screenState = viewModel.screenState
Access the state directly.
Text(
text = "Selected: ${viewModel.screenState.selectedTextFromFirstBottomSheet}",
Modifier.padding(16.dp)
)
Button(onClick = {
bottomSheetContent = {
FirstBottomSheet(
selectedText = viewModel.screenState.selectedTextFromFirstBottomSheet,
onSelected = { text ->
viewModel.onEvent(
MainScreenEvent.OnFirstBottomSheetSelectedTextChanged(text)
)
},
textList = viewModel.screenState.firstBottomSheetTextList
)
}
scope.launch {
bottomSheetState.show()
}
}, modifier = Modifier.padding(16.dp)) {
Text(text = " Open First BottomSheet")
}
Boom! It worked :)
My understanding is: you're creating a variable containing the value of a state, but you're not listening to the state changes, so the Compose doesn't know the state has changed, therefore the recomposition doesn't happen. The by keyword in your state declaration is a property delegate which set/get the current value of state, but not register the composable to react to these changes.
There are another solutions you can use to observe the state without repeat viewModel.screenState:
Using derivedStateOf:
val screenState by remember {
derivedStateOf {
viewModel.screenState
}
}
Changing the screenState declaration.
// Using "=" instead of "by"
var screenState = mutableStateOf(MainScreenState())
and then use screenState.value to set/get the state value.
And in the screen, use like below:
val screenState = viewModel.screenState

I had the exact same use case in my app but everything was working fine until I updated some of the compose libraries.

Related

How to get data and display in Jetpack Compose?

So I have my Interface, View Model, and Repository in one file. I am trying to display users from https://dummyjson.com/users but I'm having difficulty with how to setup up the Api Service & How to implement the view model in UI.
package com.example.memberlist
import androidx.lifecycle.*
import com.example.memberlist.DataSource.User
import com.example.memberlist.DataSource.Users
import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
private val retrofit = Retrofit.Builder()
.baseUrl("https://dummyjson.com/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
object UserApi {
val retrofitService: UserService by lazy { retrofit.create(UserService::class.java) }
}
interface UserService{
#GET("/users/{id}")
suspend fun getUser(#Path("id") id :Int): User
#GET("/users")
suspend fun getAllUsers():Users
}
class UserRepository constructor(
private val userService: UserService
) {
suspend fun getUser(id: Int): User {
return userService.getUser(id)
}
suspend fun getAllUsers():Users{
return userService.getAllUsers()
}
}
class UserViewModel: ViewModel() {
private val users = MutableLiveData<Users>()
val user = users as LiveData<Users>
init {
viewModelScope.launch {
try {
// Calling the repository is safe as it will move execution off
// the main thread
val user = UserApi.retrofitService.getAllUsers()
users.value = user
} catch (error: Exception) {
//
}
}
}
}
I have already managed to make the data classes by using the Json to Kotlin Plugin on Android Studio to convert the json to their data class counterparts
data class ApiDataX(
val limit: Int,
val skip: Int,
val total: Int,
val users: List<User>
)
data class User(
val address: Address,
val age: Int,
val bank: Bank,
val birthDate: String,
val bloodGroup: String,
val company: Company,
val domain: String,
val ein: String,
val email: String,
val eyeColor: String,
val firstName: String,
val gender: String,
val hair: Hair,
val height: Int,
val id: Int,
val image: String,
val ip: String,
val lastName: String,
val macAddress: String,
val maidenName: String,
val password: String,
val phone: String,
val ssn: String,
val university: String,
val userAgent: String,
val username: String,
val weight: Double
)
The next one is the MainActivity all comoposable stuff.
package com.example.memberlist
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.*
import androidx.compose.material.ButtonDefaults.buttonColors
import androidx.compose.material.ButtonDefaults.elevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.memberlist.DataSource.User
import com.example.memberlist.ui.theme.MemberListTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MemberListTheme {
// A surface container using the 'background' color from the theme
}
}
}
}
#Composable
fun Screen() {
val viewModel: UserViewModel = viewModel()
val users by viewModel.user.observeAsState()
Surface(
modifier = Modifier
.fillMaxSize()
){
Column(verticalArrangement = Arrangement.spacedBy(10.dp)){
Spacer(Modifier.height(10.dp))
Head()
LazyColumn(
modifier = Modifier
.padding(15.dp, 10.dp)
){
items(100){
Item()
}
}
}
}
}
#Composable
fun UserList(){
}
#Composable
fun Item(){
Box(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
.layoutId("box")
){
Row(
horizontalArrangement = Arrangement.spacedBy(50.dp),
verticalAlignment = Alignment.CenterVertically
){
CircleImage()
Column(
verticalArrangement = Arrangement.spacedBy(5.dp)
){
Text(
text = "John Doe",
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight(500)
)
Button(
onClick = {},
contentPadding = PaddingValues(horizontal = 40.dp, vertical = 0.dp),
modifier = Modifier
.border(1.dp, Color.Red, RectangleShape)
.height(30.dp),
colors = buttonColors(
backgroundColor = Color.Transparent
),
elevation = elevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp,
disabledElevation = 0.dp,
hoveredElevation = 0.dp,
focusedElevation = 0.dp
)
){
Text(
text = "Follow",
color = Color.Red
)
}
}
}
}
}
#Composable
fun CircleImage(){
Image(
painter = painterResource(id = R.drawable.habesha_guy_11),
contentDescription = null,
modifier = Modifier
.size(75.dp)
.clip(CircleShape)
)
}
#Composable
fun Head(){
Column() {
SearchField("Search ...")
RowSelection()
}
}
#Composable
fun RowSelection(){
Box(
modifier= Modifier
.fillMaxWidth()
.padding(10.dp),
contentAlignment = Alignment.CenterEnd
){
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.horizontalScroll(
rememberScrollState(),
true,
null
)
){
Text(
text="All",
color = Color.DarkGray,
)
Text(
text="Groups",
color = Color.DarkGray,
)
Text(
text="People",
color = Color.DarkGray,
)
Text(
text="Photos",
color = Color.DarkGray,
)
Text(
text="Videos",
color = Color.DarkGray,
)
Text(
text="Pages",
color = Color.DarkGray,
)
Text(
text="Places",
color = Color.DarkGray,
)
Text(
text="Groups",
color = Color.DarkGray,
)
Text(
text="Events",
color = Color.DarkGray,
)
Spacer(modifier = Modifier.width(20.dp))
}
}
}
#Composable
fun SearchField(
hint: String
){
val txt = rememberSaveable() {
mutableStateOf("")
}
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 45.dp, max = 50.dp),
contentAlignment = Alignment.Center
){
BasicTextField(
value = txt.value,
onValueChange = {txt.value = it},
modifier = Modifier
.clip(RoundedCornerShape(30.dp))
.fillMaxWidth(0.8f)
.fillMaxHeight()
.shadow(1.dp, RoundedCornerShape(30.dp), true),
maxLines = 1,
singleLine = true
){
if(txt.value.isEmpty())
Text(
text = hint,
color = Color.DarkGray,
modifier = Modifier
.offset(20.dp, y = 15.dp)
)
/*
SEARCH ICON
Icon(
painter = painterResource(id = R.drawable.ic_baseline_search_24),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.offset(x = 100.dp)
)
*/
}
}
}
#Preview(showBackground = true, showSystemUi = true)
#Composable
fun DefaultPreview() {
MemberListTheme {
Screen()
}
}
Here is how the UI looks in Picture, am using res/drawable image
UI image
Can someone help use the dummyjson.com/users to display on the UI. How to setup the ViewModel, Interface, and Repository and How to implement the ViewModel on a composable?

How to change background color of the jetpack compose snackbar?

I want to change solid or gradient color to jetpack compose snack bar. Please guide me how to
change color
Here is my snack bar using material3 compose, I am looking solution to change the background color
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import compose.material.theme.ui.theme.Material3ComposeTheme
import compose.material.theme.ui.theme.Purple40
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Material3ComposeTheme {
val context = LocalContext.current
val snackState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
},
content = {
fun launchSnackbar(message: String, actionLabel : String?=null, duration: SnackbarDuration = SnackbarDuration.Short){
scope.launch {
snackState.showSnackbar(message = message,actionLabel=actionLabel, duration=duration)
}
}
Column(
modifier = Modifier
.padding(it)
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(47.dp))
Text("Snackbar", Modifier.padding(bottom = 10.dp), style = MaterialTheme.typography.labelLarge)
Button(onClick = {
// * Snackbar
launchSnackbar(message = "Hi i am snackbar message", actionLabel = "Hide", duration = SnackbarDuration.Long)
}) { Text("Snackbar",style = MaterialTheme.typography.labelLarge) }
ListDividerPadding()
Text("Toast", Modifier.padding(bottom = 10.dp), style = MaterialTheme.typography.labelLarge)
Button(onClick = {
Toast.makeText(
context,
"Hi i am toast message",
Toast.LENGTH_LONG
).show()
}) { Text("Toast",style = MaterialTheme.typography.labelLarge) }
}
}
)
Box(modifier = Modifier.fillMaxSize(), Alignment.BottomCenter){
SnackbarHost(hostState = snackState)
}
}
}
}
}
You can add SnackBar composable to SnackbarHost and change colors as
SnackbarHost(hostState = snackState) {
Snackbar(
snackbarData = it,
containerColor = Color.Green,
contentColor = Color.Red
)
}
Edit
There is no overload function that takes Brush instead of Color but you can add another Composable as with gradient color or more customization via content: #Composable () -> Unit
#Composable
fun Snackbar(
modifier: Modifier = Modifier,
action: #Composable (() -> Unit)? = null,
dismissAction: #Composable (() -> Unit)? = null,
actionOnNewLine: Boolean = false,
shape: Shape = SnackbarTokens.ContainerShape.toShape(),
containerColor: Color = SnackbarTokens.ContainerColor.toColor(),
contentColor: Color = SnackbarTokens.SupportingTextColor.toColor(),
actionContentColor: Color = SnackbarTokens.ActionLabelTextColor.toColor(),
dismissActionContentColor: Color = SnackbarTokens.IconColor.toColor(),
content: #Composable () -> Unit
)
Can be used as
Snackbar {
Row(
modifier = Modifier.background(
brush = Brush.horizontalGradient(
listOf(
Color.Red,
Color.Green,
Color.Blue
)
)
)
) {
Text("Hello World")
}
}

How to transfer the selected day from the calendar with the selected day to the text?

I'm using a third party calendar library. I tried to do it through the current date, but it didn’t work out. Maybe someone knows how to transfer data from a function to another function? I need the date that the user has selected in the calendar to display as text, but I don't know how to do it (
Gradle
val androidMain by getting {
dependencies {
implementation ("com.himanshoe:kalendar:1.0.0-RC5")
implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.appcompat:appcompat:1.4.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
implementation("androidx.activity:activity-compose:1.5.0")
implementation("androidx.compose.ui:ui:1.2.0-rc03")
implementation("androidx.compose.material:material:1.2.0-rc03")
implementation("androidx.compose.ui:ui-tooling-preview:1.2.0-rc03")
implementation("androidx.compose.material:material-icons-extended:1.2.0-rc03")
implementation("com.google.accompanist:accompanist-systemuicontroller:0.24.13-rc")
implementation("io.coil-kt:coil-compose:2.1.0")
implementation("io.coil-kt:coil-gif:2.1.0")
implementation("io.insert-koin:koin-core:3.2.0")
implementation("io.insert-koin:koin-androidx-compose:3.2.0")
implementation("io.github.alexgladkov:odyssey-core:1.0.0-beta12")
implementation("io.github.alexgladkov:odyssey-compose:1.0.0-beta12")
}
DatePicker
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.himanshoe.kalendar.common.KalendarSelector
import com.himanshoe.kalendar.common.KalendarStyle
import com.himanshoe.kalendar.ui.Kalendar
import com.himanshoe.kalendar.ui.KalendarType
import java.time.LocalDate
#Composable
fun DatePicker(modifier: Modifier = Modifier, onDaySelected: (LocalDate) -> Unit) {
Box(modifier = modifier) {
Kalendar(
kalendarType = KalendarType.Firey(),
kalendarStyle = KalendarStyle(
kalendarBackgroundColor = Color.White,
kalendarColor = Color.White,
kalendarSelector = KalendarSelector.Circle(
selectedColor = Color.Black,
eventTextColor = Color.Black,
todayColor = Color.White,
selectedTextColor = Color.White
),
elevation = 0.dp
),
onCurrentDayClick = { day, _ ->
onDaySelected(day)
})
}
}
SelectedDate
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.happy.R
import java.time.LocalDate
#Composable
fun SelectedDate(
date: LocalDate,
modifier: Modifier = Modifier,
onDateClick: () -> Unit
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
Row(
modifier = modifier.clickable { onDateClick() },
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = date.toString(),
style = MaterialTheme.typography.body1
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
painter = painterResource(id = R.drawable.ic_calendar_outline_24),
contentDescription = null
)
}
}
}
DashboardUIState
import com.happy.screens.dashboard.presentation.models.TaskUi
import java.time.LocalDate
#Immutable
data class DashboardUiState(
val currentDate:LocalDate = LocalDate.now(),
val dayOfTheWeek: String = "",
val taskList: List<TaskUi> = emptyList()
) {
companion object {
val Empty = DashboardUiState()
}
}
#Immutable
sealed class DashboardUiEvent {
object OnAddTask : DashboardUiEvent()
class OnTaskClick(val id: Int) : DashboardUiEvent()
}
#Immutable
sealed class DashboardUiEffect {
object NavigateToTaskCreation : DashboardUiEffect()
class NavigateToTaskDetails(val id: Int) : DashboardUiEffect()
}
DashboardViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.time.LocalDate
class DashboardViewModel : ViewModel() {
private val _state = MutableStateFlow(DashboardUiState.Empty)
val state = _state.asStateFlow()
private val _effect = MutableSharedFlow<DashboardUiEffect>()
val effect = _effect.asSharedFlow()
init {
_state.update { it.copy(currentDate = LocalDate.now(), dayOfTheWeek = "Сегодня") }
// val exampleList = listOf(
// TaskUi(id = 0, unicode = "\uD83D\uDD25", isDone = true),
// TaskUi(id = 1, unicode = "\uD83D\uDD25", isDone = false),
// TaskUi(id = 0, unicode = "\uD83D\uDD25", isDone = true),
// TaskUi(id = 0, unicode = "\uD83D\uDD25", isDone = true),
// TaskUi(id = 0, unicode = "\uD83D\uDD25", isDone = true),
// TaskUi(id = 0, unicode = "\uD83D\uDD25", isDone = true),
// TaskUi(id = 0, unicode = "\uD83D\uDD25", isDone = true),
// TaskUi(id = 0, unicode = "\uD83D\uDD25", isDone = true),
// TaskUi(id = 0, unicode = "\uD83D\uDD25", isDone = true)
// )
// _state.update { it.copy(taskList = exampleList) }
}
fun sendEvent(event: DashboardUiEvent) {
when (event) {
DashboardUiEvent.OnAddTask -> {
viewModelScope.launch {
_effect.emit(DashboardUiEffect.NavigateToTaskCreation)
}
}
is DashboardUiEvent.OnTaskClick -> {
viewModelScope.launch {
_effect.emit(DashboardUiEffect.NavigateToTaskDetails(event.id))
}
}
}
}
}
DashboardScreen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.happy.core.navigation.Screens
import com.happy.core.ui.rememberStateWithLifecycle
import com.happy.screens.calendar.DatePicker
import com.happy.screens.calendar.DayOfTheWeekText
import com.happy.screens.calendar.SelectedDate
import com.happy.screens.dashboard.presentation.components.AddTaskButton
import com.happy.screens.dashboard.presentation.components.EmptyTaskMessage
import com.happy.screens.dashboard.presentation.components.ProfileImage
import com.happy.screens.dashboard.presentation.components.TaskItem
import org.koin.androidx.compose.getViewModel
import kotlinx.coroutines.launch
import ru.alexgladkov.odyssey.compose.extensions.present
import ru.alexgladkov.odyssey.compose.local.LocalRootController
import ru.alexgladkov.odyssey.compose.navigation.modal_navigation.ModalSheetConfiguration
import java.time.LocalDate
#Composable
fun DashboardScreen(
) {
DashboardScreen(
viewModel = getViewModel(),
onTaskCreationClick = {},
onTaskClick = {}
)
}
#Composable
private fun DashboardScreen(
viewModel: DashboardViewModel,
onTaskCreationClick: () -> Unit,
onTaskClick: (Int) -> Unit,
) {
val uiState by rememberStateWithLifecycle(viewModel.state)
val systemUiController = rememberSystemUiController()
val coroutineScope = rememberCoroutineScope()
val rootController = LocalRootController.current
val modalController = rootController.findModalController()
val modalSheetConfiguration = ModalSheetConfiguration(
maxHeight = 0.7f,
cornerRadius = 20,
closeOnSwipe = true
)
var date:LocalDate =LocalDate.now()
LaunchedEffect(Unit) {
viewModel.effect.collect { effect ->
when (effect) {
DashboardUiEffect.NavigateToTaskCreation -> onTaskCreationClick()
is DashboardUiEffect.NavigateToTaskDetails -> onTaskClick(effect.id)
}
}
}
SideEffect {
systemUiController.setSystemBarsColor(color = Color.Transparent, darkIcons = true)
}
Box(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
) {
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(top = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
SelectedDate(
date = uiState.currentDate
) {
coroutineScope.launch {
modalController.present(modalSheetConfiguration, content = {
DatePicker {
modalController.popBackStack(animate = true)
}
})
}
}
ProfileImage(isAuthorized = false)
}
DayOfTheWeekText(
day = uiState.dayOfTheWeek,
modifier = Modifier.padding(start = 24.dp, top = 12.dp)
)
if (uiState.taskList.isEmpty()) {
EmptyTaskMessage(
modifier = Modifier
.fillMaxSize()
.padding(bottom = 148.dp)
.padding(horizontal = 34.dp)
)
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(44.dp),
horizontalArrangement = Arrangement.spacedBy(84.dp),
) {
items(uiState.taskList) { task ->
TaskItem(
task = task,
modifier = Modifier
.size(60.dp)
.clickable {
rootController.launch(Screens.TaskList.name)
}
)
}
}
}
}
AddTaskButton(
onClick = { viewModel.sendEvent(DashboardUiEvent.OnAddTask) },
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp)
)
}
}
DashboardScreen
Since your UI state DashboardUIState is managed by your view model DashboardViewModel, you have to notify DashboardViewModel when the date is changed by the user. The user selects the date in the DatePicker which has a callback onDaySelected: (LocalDate) -> Unit. Inside this callback you can notify your DashboardViewModel that there was a change and that the state should be updated.
In your DashboardScreen update this part of the code
SelectedDate(
date = uiState.currentDate
) {
coroutineScope.launch {
modalController.present(modalSheetConfiguration, content = {
DatePicker { selectedDate ->
modalController.popBackStack(animate = true)
viewModel.updateCurrentDate(selectedDate)
}
})
}
}
In your view model DashboardViewModel add a function that will update the state correctly with the received date value.
fun updateCurrentDate(date: LocalDate) {
val locale = Locale.getDefault() // or set your Locale if the default is not correct
val dayOfTheWeek = date.dayOfWeek.getDisplayName(TextStyle.FULL, locale)
_state.update { it.copy(currentDate = date, dayOfTheWeek = dayOfTheWeek) }
}

Left aligned custom tabs with Jetpack Compose

I need to make tabs with Jetpack Compose, looking like horizontal buttons. Tabs should be left aligned, and not centered. Just like in the image.
Also selected tab shouldn't show underline.
Jetpack compose has Scaffold for such case, something like this should work for you
enum class Tab {
Day,
Week,
Month,
}
#Composable
fun TestView(
) {
var selectedTab by remember { mutableStateOf(Tab.Day) }
Scaffold(topBar = {
Row(Modifier.padding(5.dp)) {
Tab.values().forEach { tab ->
BottomBarButton(
tab.name,
selected = selectedTab == tab,
onSelect = {
selectedTab = tab
},
)
}
}
}) {
when (selectedTab) {
Tab.Day -> Text("$selectedTab content")
Tab.Week -> Text("$selectedTab content")
Tab.Month -> Text("$selectedTab content")
}
}
}
#Composable
fun BottomBarButton(
text: String,
selected: Boolean,
onSelect: () -> Unit
) {
Text(
text,
modifier = Modifier
.background(
if (selected)
Color.Green
else
Color.Transparent
)
.clickable(onClick = onSelect)
.padding(10.dp)
)
}
If you need bottom bar, just replace topBar = { with bottomBar = {
See more about Scaffold
How about this one?
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = colorResource(id = R.color.white),
divider = { TabRowDefaults.Divider(color = colorResource(id = R.color.transparent)) },
edgePadding = 0.dp
) {
//draw your tab
}
Try this out
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
#Composable
fun RecentTabs(tabs: List<CustomTabs>, onSelectedTab: (CustomTabs) -> Unit) {
var selectedTab by remember { mutableStateOf("") }
#Composable
fun RecentTabItem(text: String, selectedColor: Color = Color.Green, onSelect: () -> Unit) {
val selected = text == selectedTab
Text(
text,
modifier = Modifier
.clip(CircleShape)
.background(
if (selected)
selectedColor
else
Color.Transparent
)
.clickable(
onClick = {
selectedTab = text
onSelect.invoke()
}
)
.padding(vertical = 8.dp, horizontal = 18.dp)
)
}
Row(
Modifier
.scrollable(rememberScrollState(), orientation = Orientation.Horizontal)
.padding(horizontal = 5.dp, vertical = 8.dp)) {
tabs.forEach {
RecentTabItem(text = it.name, selectedColor = it.color.toColor(Color.Magenta)) { onSelectedTab.invoke(it) }
Spacer(modifier = Modifier.width(5.dp))
}
}
}
Usage:
RecentTabs(tabs = listOf(
CustomeTabs(1,"Tab1", color = Color.Blue.toString()),
CustomeTabs(2,"Tab2", color = Color.Gray.toString()),
CustomeTabs(3,"Tab3", color = Color.Red.toString())
), onSelectedTab = {
Log.d(TAG, "RecentScreen() called ${it.toString()}")
})

LazyColumn is not keeping the state of items when scrolling

Following the Pathway code labs from Google about Jetpack compose, I was trying out this code
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.BasicsCodelabTheme
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
MyScreenContent()
}
}
}
}
#Composable
fun MyApp(content: #Composable () -> Unit) {
BasicsCodelabTheme {
Surface(color = Color.Yellow) {
content()
}
}
}
#Composable
fun MyScreenContent(names: List<String> = List(1000) { "Hello Android #$it" }) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
NameList(names, Modifier.weight(1f))
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(items = names) { name ->
Greeting(name = name)
Divider(color = Color.Black)
}
}
}
#Composable
fun Greeting(name: String) {
var isSelected by remember { mutableStateOf(false) }
val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
Text(
text = "Hello $name!",
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = { isSelected = !isSelected })
)
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
colors = ButtonDefaults.buttonColors(
backgroundColor = if (count > 5) Color.Green else Color.White
)
) {
Text("I've been clicked $count times")
}
}
#Preview("MyScreen preview")
#Composable
fun DefaultPreview() {
MyApp {
MyScreenContent()
}
}
I have noticed that LazyColumn will recompose the items whenever they become Visible on the screen (intended behaviour!) however, the Local state of Greeting widget is completely lost!
I believe this is a bug in Compose, Ideally the composer should consider the remember cached state. Is there an elegant way to fix this?
Thanks in advance!
[Update]
Using rememberSaveable {...} gives the power to survive the android change of configuration as well as the scrollability
Docs
Remember the value produced by init.
It behaves similarly to remember, but the stored value will survive the activity or process recreation using the saved instance state mechanism (for example it happens when the screen is rotated in the Android application).
The code now is more elegant and shorter, we don't even need to hoist the state, it can be kept internal. The only thing I am not so sure of now with using rememberSaveable is if there will be any performance penalties when the list grows bigger and bigger, say 1000 items.
#Composable
fun Greeting(name: String) {
val isSelected = rememberSaveable { mutableStateOf(false) }
val backgroundColor by animateColorAsState(if (isSelected.value) Color.Red else Color.Transparent)
Text(
text = "Hello $name! selected: ${isSelected.value}",
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = {
isSelected.value = !isSelected.value
})
)
}
[Original Answer]
Based on #CommonWare's answer The LazyColumn will dispose the composables along with their states when they are off-screen this means when LazyColumn recomposes the Compsoables again it will have fresh start state. To fix this issue all that has to be done is to hoist the state to the consumer's scope, LazyColumn in this case.
Also we need to use mutableStateMapOf() instead of MutableMapOf inside the remember { ... } lambda or Compose-core engine will not be aware of this change.
So far here is the code:
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
val selectedStates = remember {
mutableStateMapOf<Int, Boolean>().apply {
names.mapIndexed { index, _ ->
index to false
}.toMap().also {
putAll(it)
}
}
}
LazyColumn(modifier = modifier) {
itemsIndexed(items = names) { index, name ->
Greeting(
name = name,
isSelected = selectedStates[index] == true,
onSelected = {
selectedStates[index] = !it
}
)
Divider(color = Color.Black)
}
}
}
Happy composing!

Categories

Resources