How to get data and display in Jetpack Compose? - android

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?

Related

Issue in preview , Jetpack Compose Android

A lot of errors showing on API ,
Showing Error in LazyVerticalGrid section :
This foundation API is experimental and is likely to change or be removed in the future.
In weekdays section :
Field requires API level 26 (current min is 21): `java.time.DayOfWeek#SUNDAY`
In months section there is this error:
Field requires API level 26 (current min is 21): `java.time.Month#JULY`
This is my code below .
package com.example.android.weatherapp.ui.components
import android.os.Build
import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.GridCells
import com.example.android.weatherapp.R
import androidx.compose.foundation.lazy.GridItemSpan
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.android.weatherapp.ui.theme.WeatherAppTheme
import java.time.DayOfWeek
import java.time.format.TextStyle
import java.util.*
data class Month(
val title : String ,
val dateForecasts: List<DateForeCast>
)
data class DateForeCast(
val date: String,
val highTemperature: String,
#DrawableRes val icon: Int
)
#Composable
fun Calendar(
months : List<Month>,
modifier: Modifier = Modifier
){
val gridCellNumber = 7
LazyVerticalGrid(
modifier = modifier ,
cells = GridCells.Fixed(gridCellNumber)
){
months.forEach { month ->
item(
span = { GridItemSpan(gridCellNumber) }
) { MonthLabel(month = month.title) }
items(month.dateForecasts){
DateCard(dataForecasts = it )
}
}
}
}
#Composable
fun MonthLabel(
month: String,
modifier: Modifier = Modifier
){
val weekdays = listOf(
DayOfWeek.SUNDAY,
DayOfWeek.MONDAY,
DayOfWeek.TUESDAY,
DayOfWeek.WEDNESDAY,
DayOfWeek.THURSDAY,
DayOfWeek.FRIDAY,
DayOfWeek.SATURDAY
).map{
it.getDisplayName(TextStyle.SHORT, Locale.getDefault())
}
Column(
modifier = modifier.fillMaxWidth()
) {
Text(
text = month ,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h5.copy(fontWeight = FontWeight.SemiBold)
)
Row(modifier = Modifier.fillMaxWidth() ){
weekdays.forEach{
Text(
text = it,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.body2,
modifier = Modifier.weight(1f,true)
)
}
}
}
}
#Preview(
showBackground = true
)
#Composable
fun CalendarPreview(){
val months = listOf(
Month(
title = "July",
dateForecasts = List(java.time.Month.JULY.length(false)){
DateForeCast(
date = "$it + 1",
highTemperature = "70",
icon = R.drawable.cloud_cloudy_day_forecast_sun_icon
)
}
),
Month(
title = "August",
dateForecasts = List(java.time.Month.AUGUST.length(false)){
DateForeCast(
date = "$it + 1",
highTemperature = "70",
icon = R.drawable.cloud_cloudy_day_forecast_sun_icon
)
}
)
)
WeatherAppTheme {
Calendar(months = months )
}
}
#Composable
fun DateCard(
dataForecasts: DateForeCast,
modifier: Modifier = Modifier
) = Card (
modifier = modifier,
){
Column (
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally
){
Text(
text = dataForecasts.date,
style = MaterialTheme.typography.overline,
color = Color.Blue
)
Icon(
painter = painterResource(id = dataForecasts.icon ),
contentDescription = null,
modifier = Modifier
.padding(vertical = 16.dp)
.size(24.dp),
tint = Color.Unspecified
)
}
}
This is mainly the issue of SDK, but still its not working properly.
I tried the inbuild suggestions to remove the error but it's still no progress.
I have the solution for this.
You just have to change your compileSDK and targetSDK to 33 in gradle build.
Then just clear the cache and your build will work properly .

Why jetpack compose text colors looks different from the preview?

I am new to Jetpack compose. I am following the tutorial and the problem is that according to the tutorial the text color is black on the preview as well as on the real device (in the tutorial)
on my preview I also see the color as black, however, when I run the app on my two different devices I see the color is white.
What am I missing here?
UPD
package com.example.composableexmp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.composableexmp.ui.theme.ComposableExmpTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
TopHeader()
}
}
}
}
#Composable
fun MyApp(content: #Composable () -> Unit) {
ComposableExmpTheme {
Surface(
color = MaterialTheme.colors.background
) {
content()
}
}
}
#Preview
#Composable
fun TopHeader(totalPerPerson: Double = 0.0) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.clip(shape = RoundedCornerShape(corner = CornerSize(12.dp))),
color = Color(color = 0xFFE9D7F7)
) {
Column(
modifier = Modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val total = "%.2f".format(totalPerPerson)
Text(
text = "Total Per Person",
style = MaterialTheme.typography.h5
)
Text(
text = "$$total",
style = MaterialTheme.typography.h4,
fontWeight = FontWeight.ExtraBold
)
}
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
ComposableExmpTheme {
MyApp {
Text(text = "Hello Again")
}
}
}
Colors change based on your theme. You should inspect Theme.kt in ui.theme folder.
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
And how these colors are selected
#Composable
fun MyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: #Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
On Surface is the color of the Text provided by Surface. Based on your theme it can be the values set to these colors.

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

Multiple ModalBottomSheet in Compose not updated when state changes

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.

Categories

Resources