I am having a lazy vertical grid which is rendered with a mutable list of items. i want alter the items properties in clicking in it.
Classes used:
Screen State
#Keep
data class ScreenState(
val isLoading: Boolean = false,
var items: MutableList<Item> = mutableListOf(),
val error: String? = null,
val endReached: Boolean = false,
val page: Int = 0,
val isInitialLoad : Boolean= true,
val isEmpty:Boolean = false,
val isFilterApplied:Boolean = false
)
List item data class:
#Keep
data class Item(
val id:Int,
var isLIked: Boolean = false,
var name: String? = "",
var desig: String = ",
var imageUrl: String = "",
)
Inside viewModel: create a variable to store the state
var state by mutableStateOf(ScreenState())
On Api response success i am adding the items from json to the list of state object.
state = state.copy(
items = ((state.items + itemsFromResposne) as MutableList<AllDoctorsResponse.Data.Doctor>),
page = newKey,
endReached = items.isEmpty(),
isEmpty = (state.page == 0 && items.isEmpty() )
)
And adding this state.items in a lazy vertical grid
LazyVerticalGrid( //doctors list grid
state = rememberLazyGridState(),
cells = GridCells.Fixed(2),
modifier = Modifier.zIndex(2f)
.padding(top = 20.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalArrangement = Arrangement.spacedBy(17.dp),
content = {
itemsIndexed(screenState.items) { index, item
ListItemComposable(index,item,onclick-{
// Need the code to update item.liked = true from view model
})
}
}
When the item is clicked i want to change the properties inside the list item object. But tits not rflecting in the screen on real time, but when its scrolled it gets recomposed and it gets reflected. How make it reflect and persist the state even when scrolled on and off .
Should i use a snapshot or please help how to do it.
This is code i used in viewmodel to update the item object
viewModelScope.launch {
val alteredList = state.items.also {
it.forEach {
if (it.id == item.id)
it.liked = false
}
}
}
//add new list as all doctors list
state = state.copy(
items = alteredList
)
#Keep
data class Item(
val id:Int,
val isLiked: MutableState<Boolean> = mutableStateOf(false),
var name: String? = "",
var desig: String = "",
var imageUrl: String = "",
)
Or
#Keep
class Item(
val id:Int,
isLiked: Boolean = false,
var name: String? = "",
var desig: String = "",
var imageUrl: String = "",
){
var isLiked by mutableStateOf(isLiked)
}
Related
I have a Grid with two columns and 6 elements, but I need that the user select only 3 elements
In the first #Composable I put the value in this way:
LazyVerticalGrid(
columns = GridCells.Fixed(2),
) {
items(list.values.size) {
InterestsItems(
value = list.values[it].text,
)
}
}
And the second #Composable(InterestsItems) is a Box with an Image inside. I put the value like this:
var isSelected by remember { mutableStateOf(false) }
Box(modifier = Modifier.noRippleClickable { isSelected = !isSelected })
The result is that I select all the elements, is not what I want.
You can create a data class to hold selected flag for any item
data class InterestsItem(val text: String, val isSelected: Boolean = false)
A ViewModel that keeps items and indexes of selected items and function to toggle between selected and not selected
class InterestsViewModel : ViewModel() {
val interestItems = mutableStateListOf<InterestsItem>()
.apply {
repeat(6) {
add(InterestsItem(text = "Item$it"))
}
}
private val selectedItems = mutableListOf<Int>()
fun toggleSelection(index: Int) {
val item = interestItems[index]
val isSelected = item.isSelected
if (isSelected) {
interestItems[index] = item.copy(isSelected = false)
selectedItems.remove(index)
} else if (selectedItems.size < 3) {
interestItems[index] = item.copy(isSelected = true)
selectedItems.add(index)
}
}
}
MutableStateListOf will trigger recomposition when we change item
#Composable
private fun SelectItemsFromGridSample(interestsViewModel: InterestsViewModel) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(interestsViewModel.interestItems) { index, item ->
InterestsItemCard(interestsItem = item) {
interestsViewModel.toggleSelection(index)
}
}
}
}
And some Composable with callback to pass that item is clicked
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun InterestsItemCard(interestsItem: InterestsItem, onClick: () -> Unit) {
ElevatedCard(
modifier = Modifier.size(100.dp),
onClick = onClick
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = interestsItem.text, fontSize = 30.sp)
if (interestsItem.isSelected) {
Icon(
modifier = Modifier
.size(50.dp)
.background(Color.Green, CircleShape),
imageVector = Icons.Default.Check,
tint = Color.White,
contentDescription = null
)
}
}
}
}
Result
You are apparently using a common isSelected variable for all the items. If you use a loop to render the items, move the variable declaration inside the loop.
Take the basic programming codelabs on the Android Developers Official Website, to walk through the basics. A strong base in general programming is foundational to learning about any specific programming paradigms/fields. Definitely take the Compose Pathway, now renamed to the Jetpack Compose Course, before anything. In this scenario, you'll need to create either a list representing the selected items, or a hash-map, which is basically a list in which each element is a pair of two objects. When the size of the list/map gets to 3, handle the event inside the on-click, to prevent further selection. That'll depend on what you want to do, and should be custom to your project.
Put the isSelected variable inside the items block.
Example;
val list = remember { mutableStateOf(listOf("0","1","2","3","4","5") )}
LazyVerticalGrid(
columns = GridCells.Fixed(2),
) {
items(list.value.size){
var isSelected by remember { mutableStateOf(false) }
Text(
modifier = Modifier.clickable{
isSelected = !isSelected
},
text = list.value[it],
color = if (isSelected) Color.Red else Color.Black
)
}
}
Result;
My Coil Image Implementation
val painter = rememberCoilPainter(
request = category.icon,
imageLoader = ImageLoader.invoke(context)
)
Image(
painter = painter ?: painterResource(id = R.drawable.home_active),
contentDescription = "",
modifier = modifier.size(50.dp)
)
And Data Source
data class Category(
val icon: String?,
val id: Int,
val is_subcat: String,
val name: String?,
val type: String
) {
companion object {
val fakeCategory = listOf(
Category(
id = 7,
name = "Malls",
icon = "http://appsinvodevlopment.com/walk_in/public/category_images/2.png",
is_subcat = "1",
type = "0"
),
...
}
why goes here
And if someone has time please check my code, becuase i got nullPointerExeception for that reason i use fake data list
I built a fragment, using jetpack compose for adding views on screen and state and view model classes for saving state.
first time when I navigate to this fragment, fetch API is called and If I have any value on this API, I filled text fields with them. It is possible that the API is empty and do not return any value.
Users can enter any data on this text fields or can ignore them because filling the text fields is optional and when he/she clicks on submit button; the data is saved in server by calling save API and shows a successful message that data is saved.
my question is that when user navigates to this fragment and does not enter or change any value,
when clicks on submit button, I do not want to show that toast.
how can I handle it? thanks.
First you will need to create a data class to hold the values for your screen as a mutable state with a default value.
data class SampleState(
val isLoading: Boolean = true,
val someText1: String = "",
val someText2: String = ""
)
It is also necessary to create a sealed class with the possible screen actions, such as changing someText1 and someText2 and also the click of a button.
sealed class SampleAction {
object TestButton : SampleAction()
data class ChangeSomeText1(val text1: String) : SampleAction()
data class ChangeSomeText2(val text2: String) : SampleAction()
}
And finally, a sealed class to contain the possible one-time-event that can be triggered through some action, like after clicking on the button with the fields properly filled in.
sealed class SampleChannel {
object ValidFields : SampleChannel()
}
The view model will be responsible for managing all the logic:
#HiltViewModel
class SampleViewModel #Inject constructor() : ViewModel() {
var state by mutableStateOf(value = SampleState())
private set
private val _channel = Channel<SampleChannel>()
val channel = _channel.receiveAsFlow()
init {
loadInitialFakeData()
}
fun onAction(action: SampleAction) {
when (action) {
is SampleAction.TestButton -> testButtonClicked()
is SampleAction.ChangeSomeText1 -> state = state.copy(someText1 = action.text1)
is SampleAction.ChangeSomeText2 -> state = state.copy(someText2 = action.text2)
}
}
private fun loadInitialFakeData() = viewModelScope.launch {
delay(timeMillis = 1000)
if (Random.nextBoolean()) state = state.copy(
someText1 = "fake text 1 from api",
someText2 = "fake text 2 from api"
)
state = state.copy(isLoading = false)
}
private fun testButtonClicked() {
val hasBlankField = listOf(
state.someText1,
state.someText2
).any { string ->
string.isBlank()
}
if (hasBlankField) return
viewModelScope.launch {
_channel.send(SampleChannel.ValidFields)
}
}
}
In this case there is a loadInitialFakeData function that is executed as soon as the view model is instantiated, which simulates a delay only for testing purposes and according to a random boolean assigns or not values in the state class.
There is also a function testButtonClicked that will do the logic if any of the fields is empty, if any of them is empty nothing happens (I left it that way so as not to extend the code too much) and if it is not empty we notify the channel with the ValidFields.
The only public function in the view model is the onAction, which is responsible for receiving actions through the composable screen.
Now on the composable screen we can do it like this:
#Composable
fun SampleScreen(
viewModel: SampleViewModel = hiltViewModel()
) {
val state = viewModel.state
val scaffoldState = rememberScaffoldState()
LaunchedEffect(key1 = Unit) {
viewModel.channel.collect { channel ->
when (channel) {
SampleChannel.ValidFields -> {
scaffoldState.snackbarHostState.showSnackbar(
message = "all right!"
)
}
}
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) {
Column(modifier = Modifier.fillMaxSize()) {
ComposeLoading(isLoading = state.isLoading)
ComposeForm(
changeSomeText1 = {
viewModel.onAction(SampleAction.ChangeSomeText1(it))
},
changeSomeText2 = {
viewModel.onAction(SampleAction.ChangeSomeText2(it))
},
testButtonClick = {
viewModel.onAction(SampleAction.TestButton)
},
state = state
)
}
}
}
#Composable
private fun ComposeLoading(
isLoading: Boolean
) = AnimatedVisibility(
modifier = Modifier.fillMaxWidth(),
visible = isLoading,
enter = expandVertically(animationSpec = tween()),
exit = shrinkVertically(animationSpec = tween())
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(height = 16.dp))
Text(
text = "loading...",
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.body2
)
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
#Composable
private fun ComposeForm(
changeSomeText1: (String) -> Unit,
changeSomeText2: (String) -> Unit,
testButtonClick: () -> Unit,
state: SampleState
) = Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
ComposeSimpleTextField(
value = state.someText1,
onValueChange = {
changeSomeText1(it)
},
label = "text field 1"
)
Spacer(modifier = Modifier.height(height = 16.dp))
ComposeSimpleTextField(
value = state.someText2,
onValueChange = {
changeSomeText2(it)
},
label = "text field 2"
)
Spacer(modifier = Modifier.height(height = 16.dp))
Button(
modifier = Modifier.align(alignment = Alignment.End),
onClick = testButtonClick
) {
Text(text = "Test Button")
}
}
#Composable
private fun ComposeSimpleTextField(
value: String,
onValueChange: (String) -> Unit,
label: String
) = TextField(
modifier = Modifier.fillMaxWidth(),
value = value,
onValueChange = { onValueChange(it) },
label = {
Text(text = label)
}
)
The main thing here is the LaunchedEffect to collect all possible events from the SampleChannel (on this example there is only one) and the calls to screen events when the text of some field is changed or when the button is clicked... The rest is just a compose boilerplate that doesn't matter much for the main logic.
Note: I used Hilt for dependency injection in this example, so I had access to #HiltViewModel and hiltViewModel()
implementation 'com.google.dagger:hilt-android:2.42'
kapt 'com.google.dagger:hilt-android-compiler:2.42'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
I have two tables like as I described below
object Categories : LongIdTable() {
val name = varchar(name = "name", length = 50).uniqueIndex()
val count = integer(name = "count")
val page = integer(name = "page")
}
object Images : LongIdTable() {
val color = varchar(name = "color", length = 100)
val blurHash = varchar(name = "blurHash", length = 200)
val unsplashId = varchar(name = "unsplashId", length = 200)
val category = reference(name = "category", Categories.name).nullable()
val isAvailable = bool(name = "isAvailable")
val imagePath = varchar(name = "imagePath", length = 500)
}
and here is my Image & Category data classes
#Serializable
data class Image(
val id: Long,
val unsplashId: String,
val isAvailable: Boolean = false,
val color: String,
val blurHash: String,
var category: Category?,
val imagePath: String,
)
#Serializable
data class Category(
val id: Long,
val name: String,
val count: Int,
var imagePath: String?,
val page: Int
)
when I want to find images by their id I can't map category attribute
fun getImageById(id: Long): Image {
val images = ArrayList<Image>()
var categoryName = ""
transaction {
Images.select {
Images.id eq id
}.map { row ->
categoryName = row[Images.category]!!
images.add(
Image(
id = row[Images.id].value,
unsplashId = row[Images.unsplashId],
isAvailable = false,
color = row[Images.color],
blurHash = row[Images.blurHash],
category = null,
imagePath = row[Images.imagePath]
)
)
}
images[0].category = categoryRepository.getCategoryByName(categoryName)
}
return images[0]
}
how can I solve this priblem?
I am tring to pass a list of datas from a fragment to a activity using intent. When sending the data from fragment, the data is exist. But in activity the its says data is null. I am not sure if the way i pass the data is correct or not or is there any better way to pass the data from fragment to activity?
In Fragment
private var selectedBankCard: Channels = Channels //get a list of data from BE
override fun onClick(view: View?) {
when (view?.id) {
R.id.bankcard_layout -> {
try {
if (activity != null) {
val intent =
Intent(context, BankCardListActivity::class.java)
if (selectedBankCard != null) {
intent.putExtra("BANKCARDINFORMATION", selectedBankCard) //upon debug, i can see a selectedBankCard is not null
}
startActivityForResult(intent, 10001)
}
} catch (e: Exception) {
}
}
}
}
}
In Activity
class AutoReloadBankCardListActivity : BaseActivity() {
private lateinit var channels: Channels
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.bank_card)
if (intent == null) {
return
}
channels = intent.getSerializableExtra("BANKCARDINFORMATION") as Channels // upon debug, getSerializableExtra returns null
}
class Channels : Serializable {
var newCard = false
var disable = false
var disableCode: String = ""
var description: String = ""
var channelType: String = ""
var payBrands: List<String>? = null
var channelIndex: String = ""
var payToolType: String = ""
var selected = false
var payBrand: String = ""
var extendInfo: ExtendInfo? = null
var disableReason: String = ""
var icon: String = ""
var chargeIndex: String = ""
}
better way to pass the data from fragment to activity?
Better way is listener. You can use interface or lambda in kotlin.
Your fragment. Category is your data/object in this example
class MyFragment : Fragment() {
var listener: ((category: Category) -> Unit)? = null
fun sendCategoryToListener(category: Category) {
listener?.invoke(category)
}
}
Code in your activity
val fragment = MyFragment()
fragment.listener = { category ->
// Do what you want with your object
}
Can you provide the class definition of selectedBankCard?
The type of selectedBankCard must implement the Serializable interface, otherwise the data cannot be obtained in the Activity.
data class Channels(
var newCard = false,
var disable = false,
var disableCode: String = "",
var description: String = "",
var channelType: String = "",
var payBrands: List<String>? = null,
var channelIndex: String = "",
var payToolType: String = "",
var selected = false,
var payBrand: String = "",
var extendInfo: ExtendInfo? = null,
var disableReason: String = "",
var icon: String = "",
var chargeIndex: String = ""
) : Serializable
if you use this code to start a fragment from an activity then you can easily get the return data like this.
Code to start fragment
fun addFragment(
fragment: Fragment,
) {
supportFragmentManager.beginTransaction()
.add(R.id.nav_host_fragment, fragment)
.addToBackStack(BACK_STACK)
.commit()
}
addFragment(ABCFragment(item->{//here the item is your return data}))
You can add fragment like this
and to get return data you can add fragment like this
addFragment(ABCFragment( private val returnData: (item: ABCModel) -> Unit,))
and in fragment write this code
class ABCFRagment(private val returnData: (item: ABCModel) -> Unit,):Fragment{
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
item.invoke(data)
}}
and in activity you can get your return data as item