Android, Jetpack Compose - Composable function not getting refreshed - android-jetpack-compose

I am new to Android Jetpack compose.
All i want to do is to update data in UI when it gets from API.
Here is my Composable function:
#Composable
fun getCurrentWeather(lat : Double, long : Double, nc : NavHostController,
vm: CurrentConditionsViewModel = hiltViewModel()) {
vm.getWeather(lat = lat, long = long)
var check by remember { mutableStateOf("") }
var text by remember { mutableStateOf("") }
val state = vm._state.value
when (state) {
is Resource.Loading -> {
CircularProgressIndicator(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(align = Alignment.Center)
)
check = "hi"
}
is Resource.Success -> {
for (i in state.data!!.weather) {
print("called")
}
state.data.name.let {
text = it.toString()
}
}
is Resource.Error -> {
Log.d("StateError", state.message.toString())
}
}
}
And here is my ViewModel:
#HiltViewModel
class CurrentConditionsViewModel #Inject constructor(private val weatherRepository: WeatherRepositoryImpl) :
ViewModel() {
val _state: MutableState<Resource<CurrentConditionsDataModel>?> = mutableStateOf(null)
fun getWeather(lat : Double, long : Double) {
viewModelScope.launch {
weatherRepository.getWeather(latt = lat, longg = long).collect {
_state.value = it
}
}
}
}
Everything is going fine but the only problem is that Composable function is not getting called state variable is updated in view model. I have tried using other variables too such as check and text, yet Composable is only getting called once.
What am I doing wrong? I there anything I have missed?
Thanks in advance.

Related

How to create a JSpinner-like widget in Jetpack Compose

I would like to create a widget in Jetpack Compose (Desktop) with similar functionality as the JSpinner in Swing, i.e. an editable text field and two buttons that increase/decrease the value in the text field. Also, I would like
the value to be validated and to be saved when the spinner loses its focus
the buttons not to be skipped in the navigation, so that the user can navigate directly between multiple spinner text fields
After a lot of trial and error I have figured out the following working version, but I wonder if there is a simpler or more elegant way to do this:
#Composable
fun TextFieldSpinner(
label: #Composable (() -> Unit)?,
lastText: String,
validateText: (String) -> Boolean,
commitText: (String) -> Unit,
onIncrement: () -> Unit,
onDecrement: () -> Unit
) {
val (isEditing, setEditing) = remember { mutableStateOf(false) }
// intermediateTextFieldValue is only used locally to store the temporary state of the TextField while editing
val (intermediateTextFieldValue, setIntermediateTextFieldValue) = remember { mutableStateOf(TextFieldValue(lastText))}
var isError by remember { mutableStateOf(!validateText(lastText)) }
val resetText = { setIntermediateTextFieldValue(TextFieldValue(lastText)) }
if (!isEditing && !isError && lastText != intermediateTextFieldValue.text) {
resetText()
}
val onCommit = {
if (validateText(intermediateTextFieldValue.text)) {
isError = false
commitText(intermediateTextFieldValue.text)
} else {
isError = true
}
}
val onLeaveTextField = {
setEditing(false)
onCommit()
isError = false
}
val onNewFocusState = { newFocusState: Boolean ->
setEditing(newFocusState)
if (!newFocusState)
onCommit()
}
}
TextFieldSpinnerUI(
label = label,
value = intermediateTextFieldValue,
onValueChange = { newTextFieldValue:TextFieldValue ->
setEditing(true)
setIntermediateTextFieldValue(newTextFieldValue)
// eager committing without showing error state
if (validateText(newTextFieldValue.text)) {
isError = false
commitText(newTextFieldValue.text)
}
},
modifier = modifier,
isError = isError,
onIncrement = {
onLeaveTextField()
onIncrement()
},
onDecrement = {
onLeaveTextField()
onDecrement()
},
onFocusChanged = { state ->
onNewFocusState(state.isFocused)
}
)
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
private fun TextFieldSpinnerUI(
label: #Composable (() -> Unit)?,
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
onIncrement: () -> Unit,
onDecrement: () -> Unit,
isError: Boolean,
onFocusChanged: (FocusState) -> Unit
) {
Row() {
DecreaseButton(onDecrement)
TextField(
label = label,
value = value,
singleLine = true,
isError = isError,
onValueChange = onValueChange,
modifier = Modifier.onFocusChanged(onFocusChanged)
)
IncreaseButton(onIncrement)
}
}
#Composable
private fun DecreaseButton(
onClick: () -> Unit
) {
IconButton(
onClick = onClick,
modifier = Modifier.focusProperties {this.canFocus = false }
) {
Icon(
imageVector = Icons.Rounded.Remove
)
}
}
#Composable
private fun IncreaseButton(
onClick: () -> Unit
) {
IconButton(
onClick = onClick,
modifier = Modifier.focusProperties { this.canFocus = false }
) {
Icon(
imageVector = Icons.Rounded.Add
)
}
}
In particular, it seems to be hard to have the text field at the same time
to be editable
to be validated, saved and then recomposed with the new value
to have another widget such as the buttons change its value

How can we test Jetpack Compose callback function on a click action?

The problem is:
We have a test case where we expect that a callback function was called in response to a click action in our Composable. How can we handle this test correctly?
You can try something like this:
#Test
fun test() {
var clicked = false
testRule.setContent {
YourComposable(
onClick = { clicked = true },
}
}
testRule.onNodeWithTag("YourButton")
.performClick()
assertTrue(clicked)
}
I'm not sure if you understand state hoisting,reference please try simple example:
#Composable
fun ButtonState(modifier : Modifier = Modifier,state : (flag : Boolean)->Unit) {
var isFlag by remember{ mutableStateOf(false)}
Button(onClick = {
isFlag = !isFlag
state(isFlag)
},modifier) {
Text(text = "text")
}
}
setContent {
var flag by remember { mutableStateOf(false) }
Column {
ButtonState {
flag = it
}
Text(text = "$flag")
}
}
#get:Rule
val composeTestRule = createComposeRule()
#Test
fun `should invoke callback on button click`() {
val callback : () -> Unit = mockk()
composeTestRule.setContent {
YourComposable(
onClick = callback,
)
}
composeTestRule.onNodeWithTag("BUTTON_TAG").performClick()
verify { callback() }
}

Navigating non-visible FocusRequesters in LazyColumn

I have a scrollable container (LazyColumn) and I want to navigate to the next FocusRequester in the list. But if the FocusRequester exists outside the rendered elements the FocusRequester can not be reached by FocusManger.
Code to reproduce problem:
#ExperimentalComposeUiApi
#Composable
fun exampleDesktop() { // Also works on phone but you need an external keyboard
val focusManager = LocalFocusManager.current
fun handleKeyPreview(evt: KeyEvent) : Boolean {
return if (evt.type == KeyEventType.KeyDown && evt.key == Key.Tab) {
if (evt.isShiftPressed) {
focusManager.moveFocus(FocusDirection.Up)
} else {
focusManager.moveFocus(FocusDirection.Down)
}
true
} else false
}
LazyColumn {
repeat(100) {
item {
FocusableItem(it, ::handleKeyPreview)
}
}
}
}
#ExperimentalComposeUiApi
#Composable
fun examplePhone() {
val focusManager = LocalFocusManager.current
Column {
Row {
Button(onClick = { focusManager.moveFocus(FocusDirection.Up)}) { Text("Up")}
Button(onClick = { focusManager.moveFocus(FocusDirection.Down)}) { Text("Down")}
}
LazyColumn {
repeat(100) {
item {
FocusableItem(it, handler = {false})
}
}
}
}
}
#Composable
fun FocusableItem(i: Int, handler: (KeyEvent) -> Boolean) {
val requester = remember { FocusRequester() }
TextField(
modifier = Modifier
.focusRequester(requester)
.onPreviewKeyEvent(onPreviewKeyEvent = handler),
value = "$i", onValueChange = {})
}
Is there a way to know of the existence of other focus requesters and put them into the view?

multi-dimensional state in jetpack compose

How can I make multidimensional state in compose that also effects the source the state came from?
I tried to ask this question without code previously and it wasn't clear. This is a little more code than necessary, but I will be able to communicate my problem
data class Record(
var duration: Int,
val timeStamp: Instant = Clock.System.now(),
var description: String? = null
)
data class Task(
var name: String,
var defaultAmount: Int,
val completions: MutableList<Record> = mutableListOf()
) {
fun createRecord(description: String? = null, duration: Int = defaultAmount): Record {
val record = Record(duration, description = description)
completions.add(record)
return record
}
}
class SourceOfTruth(private val thisSavesMeTyping: MutableList<Task> = mutableListOf()) :
MutableCollection<Task> by thisSavesMeTyping {
fun print() = thisSavesMeTyping.forEach { Log.d("ugh", it.toString()) }
}
class MyModel : ViewModel() {
val source: SourceOfTruth =
SourceOfTruth().apply {
add(
Task("exercise", 30).apply {
createRecord("push ups")
createRecord("sit ups")
})
add(Task("dishes", 10))
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val myModel: MyModel by viewModels()
val tasks = myModel.source.toMutableStateList()
Column {
tasks.forEach { task ->
Row {
Text(task.name)
task.completions.forEach { Text(it.timeStamp.toString().dropLast(15)) }
Button(
onClick = {
task.createRecord("dummy")
myModel.source.print()
}) { Text("now") }
}
}
Button(
onClick = {
tasks.add(Task("dummy task", 90))
myModel.source.print()
}) { Text("new Task") }
}
}
}
}
Pushing the now button updates the source in the viewmodel, but doesn't trigger recomposition. Pushing the add task button triggers recomposition, but doesn't add a task to the source of truth.
Any suggestions on how I can fix those?
If you read all that, Thank you very much. I wish I could buy you a coffee or beer.

Remember list item states while navigation in Jetpack Compose

If we create state for list item like val state = remember(it) { mutableStateOf(ItemState()) } then we loose expanded state while scrolling.
If we lift up states generating higher before LazyColumn then expand state is saving properly
val states = items.map { remember(it) { mutableStateOf(ItemState()) } }
LazyColumn(modifier = Modifier.fillMaxSize()) {....
But when we expand an item, click the button, go to details screen and then go back to items we loose expanded state.
What is the best way to save items state?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RememberStateTheme {
val navController = rememberNavController()
NavHost(navController, startDestination = "items") {
composable("items") {
Greeting(
onItemClick = { navController.navigate("details/$it") }
)
}
composable(
"details/{index}",
arguments = listOf(navArgument("index") { type = NavType.IntType })
) { backStackEntry ->
DetailsScreen(backStackEntry.arguments?.getInt("index") ?: -1)
}
}
}
}
}
}
#Composable
fun Greeting(
onItemClick: (Int) -> Unit
) {
val items = remember { (0..100).toList() }
Surface(color = MaterialTheme.colors.background) {
val states = items.map { remember(it) { mutableStateOf(ItemState()) } }
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
// If we create state here like val state = remember(it) { mutableStateOf(ItemState()) }
// then we loose expanded state while scrolling
// If we lift up states generating higher before LazyColumn then expand state
// is saving properly
//
// But when we expand an item, click the button and then go back to items we loose
// expanded state
val state = states[item]
key(item) {
Item(index = item,
state = state.value,
onClick = { onItemClick(item) },
modifier = Modifier
.fillMaxSize()
.clickable {
state.value.changeState()
}
)
}
Divider()
}
}
}
}
#Composable
fun Item(
index: Int,
state: ItemState,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Box(modifier = modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.Center)
) {
Text(
text = index.toString(),
modifier = Modifier.padding(16.dp)
)
if (state.expanded) {
Button(
onClick = onClick,
modifier = Modifier.padding(8.dp)
) {
Text(text = "Click me")
}
}
}
}
}
class ItemState {
val expanded: Boolean
get() = _expanded.value
private val _expanded = mutableStateOf(false)
fun changeState() {
_expanded.value = !_expanded.value
}
}
#Composable
fun DetailsScreen(
index: Int,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.fillMaxSize()
.background(Color.Gray.copy(alpha = 0.3f))
) {
Text(
text = index.toString(),
modifier = Modifier.align(Alignment.Center)
)
}
}
Using rememberSaveable solves the problem.
Thanks to https://stackoverflow.com/users/1424349/leland-richardson
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
val state = rememberSaveable(item) { mutableStateOf(ItemState()) }
key(item) {
Item(index = item,
state = state.value,
onClick = { onItemClick(item) },
modifier = Modifier
.fillMaxSize()
.clickable {
state.value.changeState()
}
)
}
Divider()
}
}
But when we expand an item, click the button, go to details screen and then go back to items we loose expanded state.
Correct. remember() remembers for the scope of a specific composition. This means it remembers across recompositions, but not when the composition is replaced by a separate composition. In your case, navigation replaces your Greeting() composition with a DetailsScreen() composition.
What is the best way to save items state?
Hoist the state further, to a composition that does not get replaced by navigation. In this case, that would be your root composition, where you have your rememberNavController() call.
Or, have the state be stored in a viewmodel that is scoped to your activity, or at least to that root composition.
If you want to have this state persist beyond the life of your process, I think that the vision is that we should use effects to save the state via a repository to some persistent store (e.g., JSON file) and restore the state from that store. However, I have not experimented with this approach yet.

Resources