If I use a custom calendar to select a day of the week, I want to communicate with the server and receive a response from the data stored on that day of the week to associate it with the LazyColumn Composables.
I've created the TodoItem and TodoItemList functions, but I'm asking because I don't know how to connect the values by communicating with the server and receiving the data response.
And now I have the following error.
> Task :app:compileDebugKotlin FAILED
e: D:\lotus_aos\app\src\main\java\com\example\todo_android\Screen\CalendarScreen.kt: (321, 80): Classifier 'ReadTodoResponse' does not have a companion object, and thus must be initialized here
In fact, when I clicked on a day of the week using a custom calendar, I communicated with the server to get the response value and put it in Log if the data for that day of the week was saved.
But now, when I click on a day of the week in a custom calendar, I ask because I'm not sure how to get the data stored on the server and connect it to LazyColumn to put it in.
The relevant codes are as follows.
ReadTodoRequest.kt
package com.example.todo_android.Request.TodoRequest
import com.example.todo_android.Response.TodoResponse.ReadTodoResponse
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface ReadTodoRequest {
#GET("/todo/todo/")
fun requestReadTodo(
#Header("Authorization") token: String,
#Query("year") year: Int,
#Query("month") month: Int,
#Query("day") day: Int
): Call<ReadTodoResponse>
}
ReadTodoResponse.kt
package com.example.todo_android.Response.TodoResponse
import com.google.gson.annotations.SerializedName
data class ReadTodoResponse(
#SerializedName("resultCode")
val resultCode: Int,
#SerializedName("data")
val data: ArrayList<RToDoResponse>
)
// 응답값으로 data의 디테일한 값들
data class RToDoResponse(
val id: Int,
val title: String,
val year: Int,
val month: Int,
val day: Int,
val done: Boolean,
val writer: String
)
TodoItem.kt
package com.example.todo_android.Component
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.example.todo_android.Data.Todo.ReadTodo
import com.example.todo_android.Response.TodoResponse.ReadTodoResponse
import java.time.Month
import java.time.MonthDay
import java.time.Year
//#Composable
//fun TodoList(token : String, year : Int, month : Int, day : Int) {
// TodoItem(number = 10)
//}
//
//#Composable
//fun TodoItem(number: Int) {
// Card(
// modifier = Modifier
// .padding(12.dp)
// .border(width = 2.dp, color = Color.LightGray)
// .fillMaxWidth()
// .height(80.dp)
// .background(color = Color.White),
//
// ) {
// Column(
// modifier = Modifier
// .fillMaxWidth()
// .background(color = Color.White),
// horizontalAlignment = Alignment.CenterHorizontally,
// verticalArrangement = Arrangement.Center
// ) {
// Text(
// text = "체크리스트 입니다 ${number}"
// )
// }
// }
//}
#Composable
fun TodoItem(todo: ReadTodoResponse) {
Card(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.height(45.dp),
shape = RoundedCornerShape(8.dp)
) {
Surface() {
Column(
verticalArrangement = Arrangement.Center
) {
Text(
text = todo.data.toString(),
Modifier.padding(10.dp)
)
}
}
}
}
#Composable
fun TodoItemList(todoList: List<ReadTodoResponse>) {
LazyColumn{
itemsIndexed(items = todoList) { index, item ->
TodoItem(todo = item)
}
}
}
CalendarScreen.kt
package com.example.todo_android.Screen
import android.util.Log
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.unit.dp
import com.example.todo_android.Component.TodoItemList
import com.example.todo_android.Data.Todo.CreateTodo
import com.example.todo_android.Data.Todo.UpdateTodo
import com.example.todo_android.Navigation.Action.RouteAction
import com.example.todo_android.Request.TodoRequest.CreateTodoRequest
import com.example.todo_android.Request.TodoRequest.DeleteTodoRequest
import com.example.todo_android.Request.TodoRequest.ReadTodoRequest
import com.example.todo_android.Request.TodoRequest.UpdateTodoRequest
import com.example.todo_android.Response.TodoResponse.CreateTodoResponse
import com.example.todo_android.Response.TodoResponse.DeleteTodoResponse
import com.example.todo_android.Response.TodoResponse.UpdateTodoResponse
import com.example.todo_android.Util.MyApplication
import com.himanshoe.kalendar.Kalendar
import com.himanshoe.kalendar.color.KalendarThemeColor
import com.himanshoe.kalendar.component.day.config.KalendarDayColors
import com.himanshoe.kalendar.model.KalendarDay
import com.himanshoe.kalendar.model.KalendarEvent
import com.himanshoe.kalendar.model.KalendarType
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import com.example.todo_android.Response.TodoResponse.ReadTodoResponse as Read
fun createTodo(token: String, year: String, month: String, day: String, title: String) {
var createTodoResponse: CreateTodoResponse? = null
var retrofit = Retrofit.Builder()
.baseUrl("https://plotustodo-ctzhc.run.goorm.io/")
.addConverterFactory(GsonConverterFactory.create())
.build()
var createTodoRequest: CreateTodoRequest = retrofit.create(CreateTodoRequest::class.java)
createTodoRequest.requestCreateTodo(token, CreateTodo(year, month, day, title))
.enqueue(object : Callback<CreateTodoResponse> {
// 실패 했을때
override fun onFailure(call: Call<CreateTodoResponse>, t: Throwable) {
Log.e("error", t.message.toString())
}
// 성공 했을때
override fun onResponse(
call: Call<CreateTodoResponse>,
response: Response<CreateTodoResponse>,
) {
createTodoResponse = response.body()
Log.d("createTodo", "token : " + MyApplication.prefs.getData("token", ""))
Log.d("createTodo", "resultCode : " + createTodoResponse?.resultCode)
Log.d("createTodo", "data : " + createTodoResponse?.data)
}
})
}
fun readTodo(token: String, year: Int, month: Int, day: Int) {
var readTodoResponse: Read? = null
var retrofit = Retrofit.Builder()
.baseUrl("https://plotustodo-ctzhc.run.goorm.io/")
.addConverterFactory(GsonConverterFactory.create())
.build()
var readTodoRequest: ReadTodoRequest = retrofit.create(ReadTodoRequest::class.java)
readTodoRequest.requestReadTodo(token, year, month, day)
.enqueue(object : Callback<Read> {
//실패할 경우
override fun onFailure(call: Call<Read>, t: Throwable) {
Log.e("readTodo", t.message.toString())
}
//성공할 경우
override fun onResponse(
call: Call<Read>,
response: Response<Read>,
) {
readTodoResponse = response.body()
Log.d("readTodo", "token : " + MyApplication.prefs.getData("token", ""))
Log.d("readTodo", "resultCode : " + readTodoResponse?.resultCode)
Log.d("readTodo", "data : " + readTodoResponse?.data)
}
})
}
fun updateTodo(
token: String,
year: String,
month: String,
day: String,
title: String,
done: String,
) {
var updateTodoResponse: UpdateTodoResponse? = null
var retrofit = Retrofit.Builder()
.baseUrl("https://plotustodo-ctzhc.run.goorm.io/")
.addConverterFactory(GsonConverterFactory.create())
.build()
var updateTodoRequest: UpdateTodoRequest = retrofit.create(UpdateTodoRequest::class.java)
updateTodoRequest.requestUpdateTodo(token, UpdateTodo(year, month, day, title, done))
.enqueue(object : Callback<UpdateTodoResponse> {
// 실패 했을때
override fun onFailure(call: Call<UpdateTodoResponse>, t: Throwable) {
Log.e("updateTodo", t.message.toString())
}
// 성공 했을때
override fun onResponse(
call: Call<UpdateTodoResponse>,
response: Response<UpdateTodoResponse>,
) {
if (response.isSuccessful) {
updateTodoResponse = response.body()
Log.d("updateTodo", "token : " + MyApplication.prefs.getData("token", ""))
Log.d("updateTodo", "resultCode : " + updateTodoResponse?.resultCode)
Log.d("updateTodo", "data : " + updateTodoResponse?.data)
} else {
Log.e("updateTodo", "resultCode : " + response.body())
Log.e("updateTodo", "code : " + response.code())
}
}
})
}
fun deleteTodo(
token: String,
) {
var deleteTodoResponse: DeleteTodoResponse? = null
var retrofit = Retrofit.Builder()
.baseUrl("https://plotustodo-ctzhc.run.goorm.io/")
.addConverterFactory(GsonConverterFactory.create())
.build()
var deleteTodoRequest: DeleteTodoRequest = retrofit.create(DeleteTodoRequest::class.java)
deleteTodoRequest.requestDeleteTodo(token)
.enqueue(object : Callback<DeleteTodoResponse> {
// 실패 했을때
override fun onFailure(call: Call<DeleteTodoResponse>, t: Throwable) {
Log.e("updateTodo", t.message.toString())
}
// 성공 했을때
override fun onResponse(
call: Call<DeleteTodoResponse>,
response: Response<DeleteTodoResponse>,
) {
deleteTodoResponse = response.body()
Log.d("deleteTodo", "token : " + MyApplication.prefs.getData("token", ""))
Log.d("deleteTodo", "resultCode : " + deleteTodoResponse?.resultCode)
Log.d("deleteTodo", "data : " + deleteTodoResponse?.data)
}
})
}
#ExperimentalMaterial3Api
#Composable
fun CalendarScreen(routeAction: RouteAction) {
val states = listOf(
"월간",
"주간"
)
var selectedOption by remember { mutableStateOf(states[1]) }
val onSelectionChange = { text: String -> selectedOption = text }
var isVisible by remember { mutableStateOf(true) }
val token = "Token ${MyApplication.prefs.getData("token", "")}"
// val year = "2023"
// val month = "2"
// val day = "6"
// val token = "Token ${MyApplication.prefs.getData("token", "")}"
// val title = "qkrwhdwns"
// val done = "true"
var year by remember { mutableStateOf(0) }
var month by remember { mutableStateOf(0) }
var day by remember { mutableStateOf(0) }
val title = remember { mutableStateOf("") }
val done = remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
Spacer(modifier = Modifier.height(15.dp))
Row(
modifier = Modifier
.clip(shape = RoundedCornerShape(24.dp))
.background(Color(0xffe9e9ed))
.padding(4.dp)
)
{
states.forEach { text ->
Text(
text = text,
color =
if (text == selectedOption) {
Color.Black
} else {
Color.Gray
},
fontWeight = FontWeight.Medium,
modifier = Modifier
.clip(shape = RoundedCornerShape(24.dp))
.clickable {
onSelectionChange(text)
isVisible = !isVisible
}
.background(
if (text == selectedOption) {
Color.White
} else {
Color(0xffe9e9ed)
}
)
.padding(
vertical = 5.dp,
horizontal = 16.dp,
)
)
}
}
Spacer(modifier = Modifier.height(29.dp))
AnimatedVisibility(isVisible)
{
Kalendar(
kalendarType = KalendarType.Oceanic(),
kalendarDayColors = KalendarDayColors(Color.Black, Color.Black),
kalendarThemeColor = KalendarThemeColor(
backgroundColor = Color.White,
dayBackgroundColor = Color(0xffFBE3C7),
headerTextColor = Color.Black),
onCurrentDayClick = { kalendarDay: KalendarDay, kalendarEvents: List<KalendarEvent> ->
year = kalendarDay.localDate.year
month = kalendarDay.localDate.monthNumber
day = kalendarDay.localDate.dayOfMonth
Log.d("Kalendar",
"year: ${kalendarDay.localDate.year}, month : ${kalendarDay.localDate.month}, day: ${kalendarDay.localDate.dayOfMonth}")
readTodo(token, year, month, day)
})
}
AnimatedVisibility(!isVisible) {
Kalendar(
kalendarType = KalendarType.Firey,
kalendarDayColors = KalendarDayColors(Color.Black, Color.Black),
kalendarThemeColor = KalendarThemeColor(
backgroundColor = Color.White,
dayBackgroundColor = Color(0xffFBE3C7),
headerTextColor = Color.Black),
onCurrentDayClick = { kalendarDay: KalendarDay, kalendarEvents: List<KalendarEvent> ->
year = kalendarDay.localDate.year
month = kalendarDay.localDate.monthNumber
day = kalendarDay.localDate.dayOfMonth
Log.d("Kalendar",
"year: ${kalendarDay.localDate.year}, month : ${kalendarDay.localDate.month}, day: ${kalendarDay.localDate.dayOfMonth}")
readTodo(token, year, month, day)
})
}
Spacer(modifier = Modifier.height(29.dp))
Text(text = day.toString())
TodoItemList(todoList = com.example.todo_android.Response.TodoResponse.ReadTodoResponse)
// Scaffold(floatingActionButton = {
// FloatingActionButton(onClick = { /*TODO*/ }) {
// Icon(imageVector = Icons.Default.Add, contentDescription = "todolist 추가")
// }
// }) {
// LazyColumn {
// TodoItem()
// }
// }
// Surface(
// shape = RoundedCornerShape(24.dp),
// modifier = Modifier
// .wrapContentSize()
// ) {}
// Button(
// modifier = Modifier
// .width(300.dp)
// .height(50.dp),
// colors = ButtonDefaults.buttonColors(Color(0xffFFBE3C7)),
// onClick = { readTodo(token, year, month, day) }
// ) {
// Text(text = "TODO 조회", color = Color.Black)
// }
//
// Spacer(modifier = Modifier.height(30.dp))
//
// Button(
// modifier = Modifier
// .width(300.dp)
// .height(50.dp),
// colors = ButtonDefaults.buttonColors(Color(0xffFFBE3C7)),
// onClick = { createTodo(token, year, month, day, title) }
// ) {
// Text(text = "TODO 작성", color = Color.Black)
// }
//
// Spacer(modifier = Modifier.height(30.dp))
//
// Button(
// modifier = Modifier
// .width(300.dp)
// .height(50.dp),
// colors = ButtonDefaults.buttonColors(Color(0xffFFBE3C7)),
// onClick = { updateTodo(token, year, month, day, title, done) }
// ) {
// Text(text = "TODO 수정", color = Color.Black)
// }
//
// Spacer(modifier = Modifier.height(30.dp))
//
// Button(
// modifier = Modifier
// .width(300.dp)
// .height(50.dp),
// colors = ButtonDefaults.buttonColors(Color(0xffFFBE3C7)),
// onClick = { deleteTodo(token) }
// ) {
// Text(text = "TODO 삭제", color = Color.Black)
// }
}
}
The problem lies with the line TodoItemList(todoList = com.example.todo_android.Response.TodoResponse.ReadTodoResponse). This is the same as just writing TodoItemList(todoList = ReadTodoResponse). Note that there are no () after ReadTodoResponse, so in the Kotlin world this is handled like passing an object expression. ReadTodoResponse is a data class, so you need to instantiate it: ReadTodoResonse(...).
I can see that you instantiate one when you handle the response body here: readTodoResponse = response.body(), but then it looks like you never use it. I suggest you use a StateFlow to pass your response to some state holder class, such as a ViewModel. Your compose views then can access the state from the ViewModel. Later on, this kind of architecture will make it easier to implement unit tests and to refactor your code. You can read more about app architecture here.
Related
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?
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) }
}
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.
I want to make each card clickable and navigate from screen A to B. On Screen A it contains list view of card already. At screen B, I want to display each information in detail about each card from this Api.
Api Link : https://api.test.dev3.coolbeans.studio/books
This is my API Class
interface ApiInterface {
#GET("/books")
suspend fun getBooks(): Response
companion object {
private var apiInterface: ApiInterface? = null
fun getInstance(): ApiInterface {
if (apiInterface == null) {
apiInterface = Retrofit.Builder()
.baseUrl("https://api.test.dev3.coolbeans.studio")
.addConverterFactory(GsonConverterFactory.create()).build()
.create(ApiInterface::class.java)
}
return apiInterface!!
}
}}
This is my HomeScreen
#Composable
fun HomeScreen (book: BookDetail) {
Card(
backgroundColor = Color.White, elevation = 2.dp, modifier = Modifier.padding(all = 10.dp)
) {
Column(
modifier = Modifier
.padding(all = 10.dp)
.fillMaxWidth()
) {
Column {
Text(
text = "Id: ${book.id}",
style = TextStyle(fontSize = 14.sp),
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp)
)
Text(
text = "Title: ${book.title}",
style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium),
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp)
)
Text(
text = "Description: ${book.description}",
style = TextStyle(fontSize = 14.sp, textAlign = TextAlign.Justify),
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp)
)
Text(
text = "Author: ${book.author}",
style = TextStyle(fontSize = 14.sp),
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp)
)
}
}
}
}
This is my MainActivity
class MainActivity : ComponentActivity() {
private val bookViewModel by viewModels<BookViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BookList(bookList = bookViewModel.bookListResponse)
bookViewModel.getBookList()
}
}
}
#Composable
fun BookList(bookList: List<BookDetail>) {
LazyColumn {
itemsIndexed(items = bookList) { index, item ->
HomeScreen(book = item)
}
}
}
This is my BookViewModel
class BookViewModel : ViewModel() {
var bookListResponse: List<BookDetail> by mutableStateOf(listOf())
var errorMessage: String by mutableStateOf("")
fun getBookList() {
viewModelScope.launch {
val apiInterface = ApiInterface.getInstance()
try {
apiInterface.getBooks().let{
bookListResponse = it.rows
}
} catch (e: Exception) {
errorMessage = e.message.toString()
}
}
}
}
This is my BookDetail (Model class)
data class BookDetail(
val author: String,
val description: String,
val id: String,
val thumbnailUrl: String,
val title: String
)
data class Response(
val count: Int,
val rows: List<BookDetail>
)
You can follow along this tutorial to show this data on recycler view. https://www.section.io/engineering-education/handling-recyclerview-clicks-the-right-way/
Once You have setup the recycler view and adapter then you can go ahead with the below implementation.
Now First, make sure the BookDetail class implements the Serializable interface:
class BookDetail : Serializable {
// your stuff
}
then on each item's click you can start the activity B and send BookDetails to it using intents. eg:
fun launchNextScreen(context: Context, book: BookDetail): Intent {
val intent = Intent(context, NextScreenActivity::class.java)
intent.putExtra("book_details", book)
return intent
}
startActivity(launchNextScreen())
To receive BookDetail back from on Activity B Intent you'll need to call:
val bookDetail = intent.getSerializableExtra("book_details") as? BookDetail
I'm doing an application with Jetpack Compose and Kotlin. It is an app to locate an android device. I need to implement run time permissions to follow the jetpack filosofy.
I have a menu page where there is a switch that when is activate saves the location of the device but just activate the switch it is necessary to request permissions "fine_location", "coarse_location" and "back_groundlocation". This is my menu.kt code:
LazyColumn {
item {
Row {
Box(
modifier =
Modifier.fillMaxWidth(0.8f)
)
{
Text(
color = Color.Black,
text = stringResource(R.string.location_gps),
fontSize = 30.sp,
modifier = Modifier.padding(20.dp)
)
}
Box(
modifier =
Modifier.fillMaxSize(),
contentAlignment = Alignment.CenterEnd
) {
Switch(
checked = checkedStateGps.value,
onCheckedChange = { checkedStateGps.value = it },
modifier = Modifier
.padding(20.dp),
colors= SwitchDefaults.colors(
//color of switches
checkedThumbColor = Color(0xFF00CC99),
checkedTrackColor = Color(0xFF7BB661),
uncheckedThumbColor = Color(0xFF83010B),
uncheckedTrackColor = Color(0xFFBB4C4C)
)
)
}
}
I'd want to know how can I implement accompanist permissions for this.
In Compose you can use Google's Accompanist library to request permission at runtime, just with PermissionRequired.
This is an example with camera permission but you can request any permissions you have in your manifest file as android.Manifest.permission.*
var doNotShowRationale by rememberSaveable { mutableStateOf(false) }
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
PermissionRequired(
permissionState = cameraPermissionState,
permissionNotGrantedContent = {
if (doNotShowRationale) {
Text("Feature not available")
} else {
Column {
Text("The camera is important for this app. Please grant the permission.")
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Ok!")
}
Spacer(Modifier.width(8.dp))
Button(onClick = { doNotShowRationale = true }) {
Text("Nope")
}
}
}
}
},
permissionNotAvailableContent = {
Column {
Text(
"Camera permission denied. See this FAQ with information about why we " +
"need this permission. Please, grant us access on the Settings screen."
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = navigateToSettingsScreen) {
Text("Open Settings")
}
}
}
) {
Text("Camera permission Granted")
}
Request camera permission sample:
implementation "com.google.accompanist:accompanist-permissions:0.20.0"
The permission APIs are currently experimental and they could change
at any time. All of the APIs are marked with the
#ExperimentalPermissionsApi annotation.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionRequired
import com.google.accompanist.permissions.rememberPermissionState
import pe.edu.upc.permissionscompose.ui.theme.PermissionsComposeTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PermissionsComposeTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
FeatureThatRequiresCameraPermission()
}
}
}
}
}
#OptIn(ExperimentalPermissionsApi::class)
#Composable
fun FeatureThatRequiresCameraPermission() {
var doNotShowRationale by rememberSaveable {
mutableStateOf(false)
}
val cameraPermissionState =
rememberPermissionState(permission = android.Manifest.permission.CAMERA)
val context = LocalContext.current
PermissionRequired(
permissionState = cameraPermissionState,
permissionNotGrantedContent = {
if (doNotShowRationale) {
Text("Feature not available")
} else {
PermissionNotGrantedUI(
onYesClick = {
cameraPermissionState.launchPermissionRequest()
}, onCancelClick = {
doNotShowRationale = true
})
}
},
permissionNotAvailableContent = {
PermissionNotAvailableContent(
onOpenSettingsClick = { context.openSettings() })
},
content = {
Text("Camera Permission Granted")
}
)
}
#Composable
fun PermissionNotAvailableContent(onOpenSettingsClick: () -> Unit) {
Column {
Text("Camera permission denied.")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { onOpenSettingsClick() }) {
Text("Open settings")
}
}
}
#Composable
fun PermissionNotGrantedUI(onYesClick: () -> Unit, onCancelClick: () -> Unit) {
Column {
Text("Camera is important for this app. Please grant ther permission.")
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(onClick = {
onYesClick()
}) {
Text("Yes")
}
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = {
onCancelClick()
}) {
Text("Cancel")
}
}
}
}