LazyColumn(modifier = Modifier.fillMaxWidth(), state = listState) {
//ABC Category Items
item {
ABC(componentCoordinator = componentCoordinator)
}
//DEF Category Items
item {
DEF(coordinator = screenCoordinator)
}
//IJK Category Items
item {
IJK()
}
//XYZ Category Items
item {
XYZ(coordinator = screenCoordinator)
}
}
#Composable
fun ABC(
viewModel: ViewModel = hiltViewModel(),
componentCoordinator: ComponentCoordinator
) {
LazyRow(
modifier = Modifier
.fillMaxWidth()
.height(64.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
.........
})
All ABC, DEF, IJK, XYZ are Composable methods with Column, Row, LazyRow combinations.
I have to make them all inside individual item {} in order to jump them separately on basis of their index (using listState.animateScrollToItem(int)). Now, the better way of creating this LazyColumn is with "items" (instead of "item") parameter with these Composible functions list.
something like this:
LazyColumn(
modifier = Modifier.fillMaxWidth(), state = listState
) {
items(myItems, key = { it.uri }) { item ->
....
})
What could be the Array initialization code (as these methods dont have same parameters) for this and the LazyColumn function body?
How about
var itemsList = mutableStateListOf<#Composable (vararg : Any) -> Unit>)
To store all your Composables
Related
I'm trying to create a display where users can input a duration composed of hours, minutes and seconds. I manage the state of the duration with a class i wrote called TimeData. I set the value for the textfield to the state values, however this does not get updated when it changes. I have tried alot and i can't seem to figure out why this does not work, as similar implementations work just fine.
I save the state in viewmodel, inject it into the screen (composable) the screen passes fields of the TimeData to the three composables for the hours, seconds and minutes. These components handle displaying the textfields and changing the values
I've tried changing state directly, saving state in screen file with by remember instead of in viewmodel with State, i've tried changing val to var and back in the TimeData object and it's children. And much more.
p.s. this is my first question here, so if anything is not clear, please let me know.
TimeData class (saved as state in viewmodel)
data class TimeData(
val hours: TimeUnit = TimeUnit(TimeUnits.HOURS),
val mins: TimeUnit = TimeUnit(TimeUnits.MINS),
val secs: TimeUnit = TimeUnit(TimeUnits.SECS),
) {
fun isDataEmpty() = hours.value == 0L && mins.value == 0L && secs.value == 0L
}
fun TimeData.toISOString(): String = "PT" +
"${hours.value}${hours.unit.firstLetter}" +
"${mins.value}${mins.unit.firstLetter}" +
"${secs.value}${secs.unit.firstLetter}"
fun TimeData.toDuration(): Duration = Duration.parse(this.toISOString())
TimeUnit class (this value is updated)
data class TimeUnit(
var unit: TimeUnits
) {
var value: Long = 0
set(value) {
val parsedValue = value
val min = 0
val max = when (unit) {
TimeUnits.HOURS -> 99
TimeUnits.MINS -> 60
TimeUnits.SECS -> 60
}
if (parsedValue in min..max) {
field = value
}
}
override fun toString(): String {
return if (value in 1..9) "0$value" else value.toString()
}
}
enum class TimeUnits(val firstLetter: String) {
HOURS("h"),
MINS("m"),
SECS("s")
}
State in viewmodel
private val _timeState = mutableStateOf(TimeData())
val timeState: State<TimeData> = _timeState
Texfield
BasicTextField(
modifier = Modifier.widthIn(1.dp),
value = time.value.toString(),
onValueChange = {
if (it.isNotBlank()) {
//changing state directly
time.value = it.toLong()
//letting viewmodel handel change
viewModel.onEvent(AddEditExerciseEvents.DurationValueChanged(time))
}
},
singleLine = true,
textStyle = TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Medium,
color = textColor,
textAlign = TextAlign.Center
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Changing state value in viewmodel
when (event) {
is AddEditExerciseEvents.DurationValueChanged -> {
when (event.value.unit) {
TimeUnits.HOURS -> {
_timeState.value = timeState.value.copy(
hours = event.value
)
}
TimeUnits.MINS -> {
_timeState.value = timeState.value.copy(
mins = event.value
)
}
TimeUnits.SECS -> {
_timeState.value = timeState.value.copy(
secs = event.value
)
}
}
}
I am trying to make a desktop application that allows you to search through a number of predefined locations stored in Kotlin classes in a separate directory. To accomplish this, I've used the reflections and compose-jb libraries.
The problem I've run into is that I can't figure out how to update a Column of Boxes (located in another Box component) to change when I press the search button after entering tags that I want to search by.
My code is below (for the Main.kt file) that describes the entire desktop application.
val reflections = Reflections("io.github.mobomega.project.attractions")
var display = mutableSetOf<Attraction>()
fun main() = application {
val stateVertical = rememberScrollState(0)
val stateHorizontal = rememberScrollState(0)
var state = Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
)
Window(
onCloseRequest = ::exitApplication,
title = "Search",
state = rememberWindowState(width = 2256.dp, height = 1504.dp)
) {
val count = remember { mutableStateOf(1) }
MaterialTheme {
Column {
val text = remember { mutableStateOf("") }
OutlinedTextField(
value = text.value,
singleLine = true,
onValueChange = { text.value = it },
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Row (modifier = Modifier.size(2256.dp, 50.dp), horizontalArrangement = Arrangement.Center) {
Button(modifier = Modifier.align(Alignment.Top),
onClick = {
val tags = text.value.split(", ", ",")
for (tag in tags) {
search(tag.lowercase())
println("$display have tag $tag")
}
// Setting the new value of the Box
state = create(stateVertical, stateHorizontal)
// Creates error:
// "#Composable invocations can only happen from the context of a #Composable function"
}) {
Text("Search")
}
Button (modifier = Modifier.align(Alignment.Top),
onClick = {
display.clear()
}) {
Text("Reset")
}
}
Row (horizontalArrangement = Arrangement.Center) {
Box(
modifier = Modifier.fillMaxSize()
.background(color = Color(red = 0xFF, green = 0xFF, blue = 0xFF))
.padding(10.dp)
) {
state // Creating the state Box component in the Row
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight(),
adapter = rememberScrollbarAdapter(stateVertical)
)
HorizontalScrollbar(
modifier = Modifier.align(Alignment.BottomStart)
.fillMaxWidth()
.padding(end = 12.dp),
adapter = rememberScrollbarAdapter(stateHorizontal)
)
}
}
}
}
}
}
#Composable
fun textBox(text: String = "Item") {
Box(
modifier = Modifier.height(32.dp)
.width(400.dp)
.background(color = Color(200, 0, 0, 20))
.padding(start = 10.dp),
contentAlignment = Alignment.CenterStart
) {
Text(text = text)
}
}
#Composable
fun create(stateVertical: ScrollState, stateHorizontal: ScrollState) = Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
) {
Column {
var x = 0
for (attr in display) {
x++
textBox(attr.name)
if (x < display.size) {
Spacer(modifier = Modifier.height(5.dp).align(Alignment.CenterHorizontally))
}
}
}
}
fun search(text: String) {
for (attr in reflections.getSubTypesOf(Attraction::class.java)) {
val temp = attr.getConstructor().newInstance()
println("${temp.name} has tags ${temp.tags}")
if (temp.matches(text) && (temp !in display)) {
display += temp
}
}
}
I have tried to update the value of the Box that contains all of the items that match any of the search criteria, but I have run into a number of issues, such as the "onClick" function in which I set the new value of the "state" variable (storing all of the matching items) not being a Composable function, and therefore I can't change the value.
How would I accomplish changing the value of a Component such as a Box from another Component, such as a Button?
In Compose you can't create a view like you're doing with state variable. Result of your call is just Unit, and when you later call it you should see a warning "The expression is unused". The view is added at the tree hierarchy at the moment your variable is created.
To solve your problem you need to declare display as a mutable state - it's a new thing made especially for Compose, which allows triggering recomposition when this state changes:
val display by mutableStateOf<Attraction>(setOf())
And then update like this in your search:
val mutableDisplay = mutableSetOf<Attraction>()
// for
// ...
mutableDisplay += temp
// ...
display = mutableDisplay
Note that you can't use mutable set inside your mutable state, as mutable state won't be able to track changes of this set.
To learn more about state in Compose I suggest you checking this youtube video which explains the basic principles, and Compose mental model for better understanding of how to work with it.
The Code A is from offical sample code here.
val (currentSection, updateSection) = rememberSaveable { mutableStateOf(tabContent.first().section) } is a destructuring declaration.
It's a clear that currentSection is assigned by tabContent.first().section
What is assigned to the variable updateSection? Will it be assigned by tabContent.first().content ?
Code A
#Composable
fun InterestsRoute(
...
) {
val tabContent = rememberTabContent(interestsViewModel)
val (currentSection, updateSection) = rememberSaveable {
mutableStateOf(tabContent.first().section)
}
...
}
#Composable
fun rememberTabContent(interestsViewModel: InterestsViewModel): List<TabContent> {
val uiState by interestsViewModel.uiState.collectAsState()
val topicsSection = TabContent(Sections.Topics) {
val selectedTopics by interestsViewModel.selectedTopics.collectAsState()
TabWithSections(
sections = uiState.topics,
selectedTopics = selectedTopics,
onTopicSelect = { interestsViewModel.toggleTopicSelection(it) }
)
}
...
return listOf(topicsSection, peopleSection, publicationSection)
}
enum class Sections(#StringRes val titleResId: Int) {
Topics(R.string.interests_section_topics),
People(R.string.interests_section_people),
Publications(R.string.interests_section_publications)
}
class TabContent(val section: Sections, val content: #Composable () -> Unit)
updateSection is a lambda here used to update the value of the mutable state.
Consider this example:
#Composable
fun MyButton() {
val (count, updateCount) = remember { mutableStateOf(0) }
Button(
onClick = { updateCount(count+1) }
) {
Text(text = "$count")
}
}
Here count is Int and updateCount is (Int) -> Unit. updateCount takes an Int and updates the value of the MutableState (count) to the provided value (here count+1).
The above code updates the count text by one everytime the button is clicked.
Coming back to your code, rememberSaveable { mutableStateOf (...) } creates a new MutableState. Its initial value will be tabContent.first().section. currentSection stores the value of this MutableState (initially it will be tabContent.first().section). Now if you want to update the value of this MutableState you can use the updateSection lambda. Just invoke that lambda with the new value and currentSection will be updated automatically.
I have some doubts how to realize Search Bar in my Compose application. I call my SeachView function from ContactContent function where I pass state value.
#Composable
fun ContactContent(navigateToProfile: (Contact) -> Unit) {
val contacts = remember { DataProvider.contactList }
val textState = remember { mutableStateOf(TextFieldValue("")) }
Column(){
SearchView(textState)
LazyColumn() {
items(
items = contacts,
itemContent = {
ContactListItem(contact = it, navigateToProfile)
}
...
}
In SearchView I not sure how should I call onImeActionPerformed search state as mine search state is not be recognized.
#Composable
fun SearchView(state: MutableState<TextFieldValue>) {
Surface(){
TextField(
value = state.value,
onValueChange = { value -> state.value = value},
leadingIcon = {...},
keyBoardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search
),
onImeActionPerformed = {action, softKeyboardController -> if (action == ImeAction.Search){
>HERE IS WHERE I AM NOT SURE WHAT TO DO<
DataProvider.newSearch(textState)
}
...
}
newSearch function snippet
fun newSearch (textState: MutableState<String>){
val result = repository.search(
token = token,
page = 1,
query = "chicken"
)
DataProvider.value = result
}
Maybe you have a different solution how to realize the search bar from the list with with Kotlin Compose.
Do not use it directly in the parenthesis of the onImeActionPerformed, extract it in the parent Composable right before calling the TextField. Store it in a val, then use that val inside your onImeActionPerformed. Alogside, I assume you are creating that parameter with something like () -> Unit, ok so I'm not sure of this, but I think changing that to #Composable () -> Unit, you can access it directly without extracting it in a val first. Try it out
I am trying to implement redux with Jetpack compose. The scenario looks like this:
I have a list view where I need to show data, in composable function.
#Composable
fun CreateListView(text: String) {
val listdata = state { store.state }
LazyColumn {
//some listview code here
}
}
above, I want to use the data that I got from the redux store. but the store. The subscription method is standalone, and outside the composable. where, though I am able to update the state through new data, but the changes are not reflecting back to composable listview:
// activity page outside composable
private fun storeSubscription(){
viewModel.storeSubscription = store.subscribe {
when (store.state) {
store.state = // list data from some source
}
}
}
Is it possible to update the composable, like above, from outside the function, and without sending any parameter? Since the redux store is a global one, so it should work I think.
You can use MutableLiveData outside of composable function. Use observeAsState() in composable to recompose when data changes.
private val myLive = MutableLiveData<String>()
fun nonComposableScope(){
myLive.postValue("hello")
}
#Composable
fun MyScreen(textLive:LiveData<String>){
val text: String? by textLive.observeAsState()
// use text here
}
Try something like,
#Composable
fun <T> Store<T>.asState(): State<T> {
val result = remember { mutableStateOf(store.state) }
DisposableEffect {
val unsubscribe = store.subscribe {
result.value = store.state
}
onDispose { unsubscribe() }
}
return result
}
#Composable
fun CreateListView(text: String) {
val listdata by store.asState()
LazyColumn {
//some listview code here
}
}
The exact code might differ as I don't know what redux implementation you are using.
This creates an observable state object that will be updated whenever the lambda passed to subscribe is called. Also, it will automatically unsubscribe when CreateListView is no longer part of the composition.
You have to follow the state hosting pattern
From Android Domcumentaiton
Key Term: State hoisting is a pattern of moving state up the tree to
make a component stateless.
When applied to composables, this often means introducing two
parameters to the composable:
value: T: the current value to display. onValueChange: (T) -> Unit: an
event that requests the value to change where T is the proposed new
value.
So in your case you will save the state in the upper Composable that needs to access it, and pass the value of the state and a lambda function to change it to the other Composable, you can learn more from the Official Documentation.
You could simply use a lambda like so:
(An example from an app I am working on.)
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun RumbleSearchResult(rumbleSearchResult: RumbleSearchResult, onClick: () -> Unit) {
ListItem(
headlineText = {
rumbleSearchResult.title?.let { title ->
Text(title)
}
},
supportingText = {
rumbleSearchResult.channel.let { creator ->
val text = when {
rumbleSearchResult.views > 0 -> {
"${creator.name}, ${rumbleSearchResult.views} views"
}
else -> {
creator.name ?: ""
}
}
Row {
Text(text)
if (creator.isVerified) {
Icon(
painter = painterResource(R.drawable.ic_baseline_verified_24),
tint = Color.Cyan,
contentDescription = stringResource(id = R.string.mainActivity_verified_content_description)
)
}
}
}
},
leadingContent = {
AsyncImage(
rumbleSearchResult.thumbnailSrc,
contentDescription = null,
modifier = Modifier.size(100.dp, 100.dp)
)
},
modifier = Modifier.clickable {
onClick.invoke()
}
)
Divider()
}
Main composable:
LazyColumn {
items(viewModel.searchResults) {
RumbleSearchResult(rumbleSearchResult = it) {
openDialog = true
}
}
}