I am completely new to Jetpack Compose AND Kotlin, but not to Android development in Java. Wanting to make first contact with both technologies, I wanted to make a really simple app which populates a LazyColumn with Text from an Api.
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.
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 android.provider.Settings.Global.getString
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Card
import androidx.compose.material3.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.unit.dp
import androidx.core.content.res.TypedArrayUtils.getString
import com.example.todo_android.Response.TodoResponse.ReadTodoResponse
import com.google.gson.JsonArray
import java.time.MonthDay
import java.time.Year
#Composable
fun TodoItem(Todo: ReadTodoResponse) {
Card(
modifier = Modifier
.padding(12.dp)
.fillMaxWidth()
.height(80.dp)
.background(color = Color.White)
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
text = Todo.data.toString(),
)
}
}
}
#Composable
fun TodoItemList(Todo: List<ReadTodoResponse>) {
LazyColumn {
itemsIndexed(items = Todo) { 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.lazy.LazyColumn
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.ReadTodo
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.ReadTodoResponse
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
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: ReadTodoResponse? = 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<ReadTodoResponse> {
//실패할 경우
override fun onFailure(call: Call<ReadTodoResponse>, t: Throwable) {
Log.e("readTodo", t.message.toString())
}
//성공할 경우
override fun onResponse(
call: Call<ReadTodoResponse>,
response: Response<ReadTodoResponse>,
) {
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 todoList = remember {
mutableStateListOf<ReadTodoResponse>()
}
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
todoList.clear()
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
todoList.clear()
readTodo(token, year, month, day)
})
}
Spacer(modifier = Modifier.height(29.dp))
Text(text = day.toString())
TodoItemList(Todo = todoList)
// 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)
// }
}
}
There are several things that might be helpful.
You can use val scope = rememberCoroutineScope() in #Composable to create a coroutine scope that jobs will be canceled automatically when #Composable dispose. Then you can use it like onClick = { scope.launch{ /*do network request here*/ }}
For a list, if you want to refresh the UI when its size changes, you can choose val todos = remember { mutableStateListOf<String>() }. In that case, when you call todos.add, the UI can be updated successfully.
If you want to modify todos in your TodoItem, you may write it like this:
#Composable
fun TodoItem(
item: TodoItemBean,
deleteAction: () -> Unit,
) {
Button(onClick = deleteAction) {
Text("delete")
}
}
fun deleteTodo(
token: String,
onSuccess: () -> Unit,
onFail: (e: Throwable) -> Unit
) {
/**
* deleteTodoRequest.requestDeleteTodo(token)
.enqueue(object : Callback<DeleteTodoResponse> {
// 실패 했을때
override fun onFailure(call: Call<DeleteTodoResponse>, t: Throwable) {
onFail(t)
}
// 성공 했을때
override fun onResponse(
call: Call<DeleteTodoResponse>,
response: Response<DeleteTodoResponse>,
) {
deleteTodoResponse = response.body()
if(deleteTodoResponse.code == 200){ onSuccess() }
else { onFail(...) }
}
}
*/
}
#Composable
fun TodoList() {
val todos = remember { mutableStateListOf<TodoItemBean>() }
val scope = rememberCoroutineScope()
val context = LocalContext.current
LazyColumn {
items(todos){ item ->
TodoItem(item = item, deleteAction = {
scope.launch {
deleteTodo("token", onSuccess = {
todos.remove(item)
}, onFail = {
Toast.makeText(context, "Failed to delete", Toast.LENGTH_SHORT).show()
})
}
})
}
}
}
Hope this can help you!
Related
I am trying to learn compose and retrofit and for that I am developing a very easy app, fetching jokes from a public API and showing them in a lazy list. But it is not working and I am not able to see any jokes. I am new to Kotlin and Jetpack compose. Please help me debug this.
I have a joke class
data class Joke(
val id: Int,.
val punchline: String,
val setup: String,
val type: String
)
This is the API I am GETing from:
https://official-joke-api.appspot.com/jokes/:id
This is the response:
{"type":"general","setup":"What did the fish say when it hit the wall?","punchline":"Dam.","id":1}
This is the retrofit api service:
const val BASE_URL = "https://official-joke-api.appspot.com/"
interface JokeRepository {
#GET("jokes/{id}")
suspend fun getJoke(#Path("id") id: String ) : Joke
companion object {
var apiService: JokeRepository? = null
fun getInstance(): JokeRepository {
if (apiService == null) {
apiService = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build().create(JokeRepository::class.java)
}
return apiService!!
}
}
}
This is the Jokes view model:
class JokeViewModel : ViewModel() {
private val _jokeList = mutableListOf<Joke>()
var errorMessage by mutableStateOf("")
val jokeList: List<Joke> get() = _jokeList
fun getJokeList() {
viewModelScope.launch {
val apiService = JokeRepository.getInstance()
try {
_jokeList.clear()
// for(i in 1..100) {
// var jokeWithId = apiService.getJoke(i.toString())
// _jokeList.add(jokeWithId)
// Log.d("DEBUGGG", jokeWithId.setup)
// }
var joke = apiService.getJoke("1")
_jokeList.add(joke)
}
catch (e: Exception) {
errorMessage = e.message.toString()
}
}
}
}
This is the Main Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val jokeViewModel = JokeViewModel()
super.onCreate(savedInstanceState)
setContent {
HasyamTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
JokeView(jvm = jokeViewModel)
}
}
}
}
}
This is the Joke Component and view
#Composable
fun JokeView(jvm: JokeViewModel) {
LaunchedEffect(Unit, block = {
jvm.getJokeList()
})
Text(text = jvm.errorMessage)
LazyColumn() {
items(jvm.jokeList) {
joke -> JokeComponent(joke)
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun JokeComponent(joke: Joke) {
var opened by remember { mutableStateOf(false)}
Column(
modifier = Modifier.padding(15.dp)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { },
elevation = CardDefaults.cardElevation(
defaultElevation = 5.dp
),
onClick = { opened = !opened}
) {
Text(modifier = Modifier.padding(15.dp), text = joke.setup)
}
if (opened) {
Text(modifier = Modifier.padding(15.dp), text = joke.punchline)
}
}
}
Thank you so much
The issue here is that you are not using stateFlow. The screen is not recomposed so your LazyColumn is not recreated with the updated values.
ViewModel
class JokeViewModel : ViewModel() {
var errorMessage by mutableStateOf("")
private val _jokes = MutableStateFlow(emptyList<Joke>())
val jokes = _jokes.asStateFlow()
fun getJokeList() {
viewModelScope.launch {
val apiService = JokeRepository.getInstance()
try {
var jokes = apiService.getJoke("1")
_jokes.update { jokes }
} catch (e: Exception) {
errorMessage = e.message.toString()
}
}
}
}
Joke View
#Composable
fun JokeView(jvm: JokeViewModel) {
val jokes by jvm.jokes.collectAsState()
LaunchedEffect(Unit, block = {
jvm.getJokeList()
})
Text(text = jvm.errorMessage)
LazyColumn {
items(jokes) {
joke -> JokeComponent(joke)
}
}
}
You should read the following documentation about states : https://developer.android.com/jetpack/compose/state
I have created an app using kotlin and android jetpack compose and dependency injection for creating my
retrofit and room datebase the problem is that I'm getting some data using retrofit from internet and showing them in my ui but when I want to send a request for the second time I get the information but
I don't know how to put them insted of existing list that I have .
It's like a movie app that I want to go to next page or load more data when the list ends
interface AppGameApi {
#GET("farsroid")
suspend fun getAppGameFromPage(
#Query("token") token: String = Constants.TOKEN,
#Query("action") action: String,
#Query("page") page: String
): AppGame
#GET("farsroid")
suspend fun getAppGameFromSearch(
#Query("token") token: String = Constants.TOKEN,
#Query("action") action: String = "search",
#Query("q") q: String = ""
): AppGame
#GET("farsroid")
suspend fun getAppGameFromDownload(
#Query("token") token: String = Constants.TOKEN,
#Query("action") action: String = "download",
#Query("link") link: String = ""
): AppGameDownload
}
class AppGameRetrofitRepository #Inject constructor(private val api: AppGameApi) {
private val appGames = DataOrException<AppGame,Boolean,Exception>()
private val appGamesSearch = DataOrException<AppGame,Boolean,Exception>()
private val appGamesDownload = DataOrException<AppGameDownload,Boolean,Exception>()
suspend fun getAllAppGames(page:String,action:String):DataOrException<AppGame,Boolean,java.lang.Exception>{
try {
appGames.isLoading = true
appGames.data = api.getAppGameFromPage(page = page, action = action)
if (appGames.data!!.status == 200) appGames.isLoading = false
Log.d("Mr", "getAllAppGames: ${appGames.data!!.result[0]}")
}catch (exception:Exception){
appGames.e = exception
}
return appGames
}
suspend fun getAllAppGamesSearch(q:String):DataOrException<AppGame,Boolean,java.lang.Exception>{
try {
appGamesSearch.isLoading = true
appGamesSearch.data = api.getAppGameFromSearch(q = q)
if (appGamesSearch.data!!.status == 200) appGamesSearch.isLoading = false
}catch (exception:Exception){
appGamesSearch.e = exception
Log.d("Error", "getAllAppGames: ${appGamesSearch.e!!.localizedMessage} ")
}
return appGamesSearch
}
suspend fun getAllAppGamesDownload(link:String):DataOrException<AppGameDownload,Boolean,java.lang.Exception>{
try {
appGamesDownload.isLoading = true
appGamesDownload.data = api.getAppGameFromDownload(link = link)
if (appGamesDownload.data!!.status == 200) appGamesDownload.isLoading = false
}catch (exception:Exception){
appGamesDownload.e = exception
Log.d("Error", "getAllAppGames: ${appGamesDownload.e!!.localizedMessage} ")
}
return appGamesDownload
}
}
#HiltViewModel
class AppGameRetrofitViewModel #Inject constructor(private val repository: AppGameRetrofitRepository) :
ViewModel() {
var appGame: MutableState<DataOrException<AppGame, Boolean, Exception>> =
mutableStateOf(DataOrException(null, true, Exception("")))
private val appGameSearch: MutableState<DataOrException<AppGame, Boolean, Exception>> =
mutableStateOf(DataOrException(null, true, Exception("")))
private val appGameDownload: MutableState<DataOrException<AppGameDownload, Boolean, Exception>> =
mutableStateOf(DataOrException(null, true, Exception("")))
init {
getAllAppGames("1","app")
}
fun getAllAppGames(
page: String,
action: String
): DataOrException<AppGame, Boolean, Exception> {
viewModelScope.launch {
appGame.value.isLoading = true
appGame.value = repository.getAllAppGames(page = page, action = action)
if (appGame.value.data!!.status == 200) appGame.value.isLoading = false
}
return appGame.value
}
private fun getAllAppGamesSearch(q: String = "") {
viewModelScope.launch {
appGameSearch.value.isLoading = true
appGameSearch.value = repository.getAllAppGamesSearch(q = q)
if (appGameSearch.value.data!!.status == 200) appGameSearch.value.isLoading = false
}
}
private fun getAllAppGamesDownload(link: String = "") {
viewModelScope.launch {
appGameDownload.value.isLoading = true
appGameDownload.value = repository.getAllAppGamesDownload(link = link)
if (appGameDownload.value.data!!.status == 200) appGameDownload.value.isLoading = false
}
}
}
I have tried these and my mainActivity looks like this
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android_Complete_TemplateTheme {
val appGameViewModelRoomDatabase by viewModels<RoomDatabaseViewModel>()
val appGameViewModelRetrofit by viewModels<AppGameRetrofitViewModel>()
val navController = rememberNavController()
val pageCounter = remember {
mutableStateOf("1")
}
val pageAction = remember {
mutableStateOf("game")
}
FarsroidNavigationSystem(
roomDatabaseViewModel = appGameViewModelRoomDatabase,
appGameRetrofitViewModel = appGameViewModelRetrofit,
navController = navController,
) {
if (appGameViewModelRetrofit.appGame.value.isLoading == true) {
AppGame(
status = 200,
result = listOf()
)
} else {
appGameViewModelRetrofit.getAllAppGames(
pageCounter.value,
pageAction.value
).data!!
}
}
pageCounter.value= pageCounter.value+1
}
}
}
}
#Composable
fun FarsroidNavigationSystem(
roomDatabaseViewModel: RoomDatabaseViewModel,
appGameRetrofitViewModel: AppGameRetrofitViewModel,
navController: NavHostController,
onClicked: () -> AppGame
) {
NavHost(
navController = navController,
startDestination = Screens.HomeScreen.name
) {
composable(route = Screens.HomeScreen.name) {
HomeScreen(
retrofitViewModel = appGameRetrofitViewModel,
navController = navController
){
onClicked()
}
}
composable(route = Screens.DetailScreen.name) {
DetailScreen(
roomDatabaseViewModel = roomDatabaseViewModel,
navController = navController
)
}
}
}
#Composable
fun HomeScreen(
retrofitViewModel: AppGameRetrofitViewModel,
navController: NavHostController,
onClicked: () -> AppGame
) {
Scaffold(
modifier = Modifier
.fillMaxSize(),
backgroundColor = Color.White,
contentColor = Color.DarkGray
) { paddingValues ->
Spacer(modifier = Modifier.padding(paddingValues))
if (onClicked().status != 200)
CircularProgressIndicator(modifier = Modifier.fillMaxSize(), color = Color.Red)
else
LazyColumn {
items(onClicked().result) {
ItemCard(resultAppGame = it)
}
}
Button(onClick = {
onClicked()
}) {
Text("LoadMore")
}
}
}
#Composable
fun ItemCard(
resultAppGame: ResultAppGame,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
shape = CutCornerShape(10.dp),
backgroundColor = Color.White,
contentColor = Color.DarkGray,
elevation = 10.dp
) {
Column(
modifier = Modifier.padding(10.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier
.size(150.dp)
.padding(20.dp),
elevation = 10.dp,
shape = CircleShape,
backgroundColor = Color.White,
border = BorderStroke(width = 0.5.dp, Color.LightGray)
) {
Image(
painter = rememberAsyncImagePainter(
model = resultAppGame.pic),
contentDescription = resultAppGame.pic,
contentScale = ContentScale.Fit
)
}
Divider(thickness = 1.dp, color = Color.Gray)
Text(text = resultAppGame.title, textAlign = TextAlign.Right)
Divider(thickness = 1.dp, color = Color.Gray)
Text(text = resultAppGame.description, textAlign = TextAlign.Right)
Divider(thickness = 1.dp, color = Color.Gray)
Text(text = resultAppGame.link)
}
}
}
I'm trying to save access token data to jetpack DataStore from http response with Retrofit2.
But function of DataStore that save data is asynchronous task so this function execute before token data arrive and save empty data.
this is my UI Code:
#Composable
fun LoginBtn(
navigateToHome: () -> Unit,
authenticate: () -> Unit,
loginScreenViewModel: LoginScreenViewModel
){
val context = LocalContext.current
val scope = rememberCoroutineScope()
val tokenStore = TokenStore(context)
Button(
onClick = {
scope.launch {
val token = loginScreenViewModel.requestLogin()
tokenStore.saveAccessToken(token.access_token)
tokenStore.saveRefreshToken(token.refresh_token)
}
},
colors = ButtonDefaults.buttonColors(
containerColor = Carrot
),
shape = MaterialTheme.shapes.extraSmall,
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
enabled = (loginScreenViewModel.email.value != "" && loginScreenViewModel.password.value != "")
) {
Text(text = "login", fontWeight = FontWeight.Bold, fontSize = 16.sp)
}
}
this is loginViewModel that call http request:
class LoginScreenViewModel(): ViewModel() {
private val _email = mutableStateOf("")
val email = _email
private val _password = mutableStateOf("")
val password = _password
fun setEmail(text: String) {
_email.value = text
}
fun setPassword(text: String) {
_password.value = text
}
fun requestLogin(): Token {
var token = Token("", "")
apiService.login(username = email.value, password = password.value)
.enqueue(object : Callback<LogInResponse> {
override fun onResponse(
call: Call<LogInResponse>,
response: Response<LogInResponse>
) {
Log.i("LOGIN RESPONSE", "access_token : ${response.body()?.access_token}")
token = Token(
access_token = response.body()?.access_token!!,
refresh_token = response.body()?.refresh_token!!
)
}
override fun onFailure(call: Call<LogInResponse>, t: Throwable) {
t.printStackTrace()
}
})
return token
}
}
this is DataStore code:
class TokenStore(
private val context: Context,
) {
companion object {
private val Context.datastore: DataStore<Preferences> by preferencesDataStore("token")
val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token")
val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")
}
val getAccessToken: Flow<String> = context.datastore.data
.map { preference ->
preference[ACCESS_TOKEN_KEY] ?: ""
}
suspend fun saveAccessToken(access_token: String){
context.datastore.edit { preference ->
preference[ACCESS_TOKEN_KEY] = access_token
}
}
val getRefreshToken: Flow<String?> = context.datastore.data
.map { preference ->
preference[REFRESH_TOKEN_KEY] ?: ""
}
suspend fun saveRefreshToken(refresh_token: String){
context.datastore.edit { preference ->
preference[REFRESH_TOKEN_KEY] = refresh_token
}
}
}
I'm getting data from the server and displaying it in the list, each item can be selected with one click to display the button, but I cannot close it, I can only open it.
This is item of list class
data class Task(
val deviceName: String,
val deviceId: String,
var selected :Boolean= Boolean,
)
this is data class
data class TaskStatus(
val taskList: SnapshotStateList<Task> = SnapshotStateList(),
val selectedNumber: Int = -1,
)
My ViewModel
private val _status = MutableStateFlow(TaskStatus())
val status = _status.asStateFlow()
fun getList(){
...
for(item in result){
_status.value.taskList.add(task)
}
}
fun selectTask(task: Task) {
val list = _status.value.taskList
val selectNumber = _status.value.selectedNumber
val newSelectNumber = list.indexOf(task)
if (newSelectNumber != selectNumber) {
if (selectNumber != -1) {
list[selectNumber].selected.value = false
}
}
task.selected.value = !task.selected.value
_status.update { it.copy(selectedNumber = newSelectNumber) }
}
My LazyColumn
...
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1F),
verticalArrangement = Arrangement.spacedBy(11.dp), contentPadding = PaddingValues(16.dp)
) {
items(
taskStatus.taskList,
key = { it.deviceId }) { task ->
Item(task)
}
}
#Compose
fun Item(task:Task){
Column(){
Text(text = task.name)
Text(text = task.deviceId)
if(task.selected){
Botton()
}
}
}
I can only show but not hide the button
Thank you in advance.
I can't compile your code directly so I tried to make my own implementation. I added a callback which will be triggered from your Task Item
Your TaskList composable
#Composable
fun TaskList(
taskList: SnapshotStateList<Task>,
onSelected: (Task) -> Unit
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(11.dp), contentPadding = PaddingValues(16.dp)
) {
items(
taskList,
key = { it.deviceId }) { task ->
Item(task) {
onSelected(it)
}
}
}
}
Your TaskItem Composable
#Composable
fun Item(
task:Task,
onSelected: (Task) -> Unit
){
Column(
modifier = Modifier.clickable {
onSelected(task) // selection callback
}
){
Text(text = task.deviceName)
Text(text = task.deviceId)
if(task.selected) {
Box(modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Red)) {
}
}
}
}
And I mocked a VieModel
class TaskStateHolder {
private val _status = MutableStateFlow( TaskStatus (
taskList = mutableStateListOf(
Task(
deviceName = "Device 1",
deviceId = "Device 1 ID"
),
Task(
deviceName = "Device 2",
deviceId = "Device 2 ID"
),
Task(
deviceName = "Device 3",
deviceId = "Device 3 ID"
),
Task(
deviceName = "Device 4",
deviceId = "Device 4 ID"
),
)
))
val status = _status.asStateFlow()
fun selectTask(task: Task) {
_status.update {
val list = it.taskList
val newSelectNumber = list.indexOf(task)
val iterator = list.listIterator()
while (iterator.hasNext()) {
val obj = iterator.next()
if (task.deviceId == obj.deviceId) {
iterator.set(task.copy(selected = true))
} else {
iterator.set(obj.copy(selected = false))
}
}
it.copy(selectedNumber = newSelectNumber)
}
}
}
I modified your selectedTask function, executing _status flow udpates in a single pass using the list's iterator.
Usage somewhere outside (e.g "TaskScreen")
val tasks by stateHolder.status.collectAsState()
Column {
TaskList(tasks.taskList) {
stateHolder.selectTask(it)
}
}
I have this list I am getting from Firebase and displaying in a LazyColumn but it only works when I navigate to a different fragment and press back. My lazyColumn is being composed inside de ScreenController for the bottomNavigation.
What do I have to do to display it directly?
below is my Fragment
#AndroidEntryPoint
class OpenTicketFragment : Fragment() {
private val viewModel: OpenTicketsViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
val ticketsList = viewModel.tickets.value.data
val currentUserId = viewModel.currentUserId.value
TheProjectTheme {
val navController = rememberNavController()
val title = remember { mutableStateOf("Open Tickets") }
val isDialogOpen = remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = {
isDialogOpen.value = true
}) {
Icon(Icons.Default.ExitToApp, contentDescription = null)
}
},
title = {
Text(text = title.value)
},
actions = {
IconButton(onClick = {
findNavController().navigate(R.id.action_openTicketFragment_to_profileFragment)
}) {
Icon(Icons.Default.Person, contentDescription = null)
}
}
)
},
bottomBar = {
val items = listOf(
Screen.Open,
Screen.Available,
Screen.Closed
)
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute =
navBackStackEntry?.arguments?.getString(KEY_ROUTE)
items.forEach {
BottomNavigationItem(
icon = { Icon(it.icon, contentDescription = null) },
selected = currentRoute == it.route,
label = { Text(text = it.label) },
onClick = {
navController.popBackStack(
navController.graph.startDestination, false
)
if (currentRoute != it.route) {
navController.navigate(it.route)
}
})
}
}
}
)
{
ScreenController(
navHostController = navController,
title,
ticketsList!!,
findNavController(),
currentUserId
)
AlertDialogComponent(isDialogOpen, findNavController(), viewModel)
}
}
}
}
}
}
#Composable
fun OpenTicketLazyColumn(
ticket: Ticket,
// onClick: () -> Unit,
navController: NavController,
) {
Card(
modifier = Modifier
.padding(8.dp)
.clickable {
val action =
OpenTicketFragmentDirections.actionOpenTicketFragmentToTicketDetailFragment(
ticket.ticketId,
ticket.problem,
ticket.address,
ticket.dateOpened,
ticket.description,
ticket.name,
ticket.status
)
Log.d("SSS8", ticket.ticketId)
navController.navigate(action)
},
elevation = 4.dp
) {
Row(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Column(
modifier = Modifier
.padding(8.dp)
.weight(1f)
) {
Text(
text = "Ticket Opened Date: ${ticket.dateOpened}",
fontSize = 16.sp,
)
Text(
text = "Service required: ${ticket.problem}",
fontSize = 14.sp,
)
}
Column(
modifier = Modifier
.padding(8.dp)
.weight(1f)
) {
Text(
text = "Status: ${ticket.status}",
fontSize = 16.sp,
)
Text(
text = "Address: ${ticket.address}",
fontSize = 16.sp,
)
}
}
}
}
and my composables
#Composable
fun ScreenController(
navHostController: NavHostController,
topBarTitle: MutableState<String>,
ticketList: List<Ticket>,
navController: NavController,
currentUserId: String
) {
NavHost(
navController = navHostController, startDestination = "open"
) {
composable("open") {
Log.d("SACO", currentUserId)
LazyColumn(
modifier = Modifier
.padding(8.dp)
) {
items(items = ticketList) { item ->
val status = remember { mutableStateOf(item.status)}
if (status.value == ASSIGNED && item.assignedToId == currentUserId) {
OpenTickets(
item,
navController
)
}
}
}
topBarTitle.value = "Open Tickets"
}
composable("available") {
LazyColumn(
modifier = Modifier
.padding(8.dp)
) {
items(items = ticketList) { item ->
val status = remember { mutableStateOf(item.status)}
if (status.value == OPEN) {
ClosedTickets(
item,
navController
)
}
}
}
topBarTitle.value = "Available Tickets"
}
composable("closed") {
LazyColumn(
modifier = Modifier
.padding(8.dp)
) {
items(items = ticketList) { item ->
val status = remember { mutableStateOf(item.status)}
if (status.value == CLOSED && item.assignedToId == currentUserId) {
AvailableTickets(
item,
navController
)
}
}
}
topBarTitle.value = "Closed Tickets"
}
}
}
#Composable
fun OpenTickets(
ticket: Ticket,
navController: NavController
) {
OpenTicketLazyColumn(
ticket,
navController
)
}
the viewModel
#HiltViewModel
class OpenTicketsViewModel #Inject constructor(
private val repository: WorkerRepositoryImpl,
private val firesource: FireBaseSource
) : ViewModel() {
val currentUserId = mutableStateOf("")
val tickets: MutableState<DataOrException<List<Ticket>, Exception>> = mutableStateOf(
DataOrException(
listOf(),
Exception("")
)
)
init {
getAllTickets(listOfServices)
}
private fun getAllTickets() {
viewModelScope.launch {
val ticketsList = repository.getAllTickets().data
tickets.value.data = ticketsList
}
}
the firebaseSource
class FireBaseSource #Inject constructor(
private val firebaseAuth: FirebaseAuth,
private val firestore: FirebaseFirestore
) {
suspend fun getAllTickets(
listOfServices: List<String>
): DataOrException<List<Ticket>, Exception> {
val dataOrException = DataOrException<List<Ticket>, Exception>()
try {
dataOrException.data = firestore.collection(TICKETS)
.whereEqualTo(PROBLEM, ELECTRICIAN)
.get()
.await().map { document ->
document.toObject(Ticket::class.java)
}
} catch (e: FirebaseFirestoreException) {
dataOrException.e = e
}
return dataOrException
}
}
1. List item