The Code A is from the official Sample project here.
1: I think I can wrap text.isNotBlank() withremember, so I think Code B is good, right?
BTW, I know the system will re-calculate when the text ( val (text, setText) = remember { mutableStateOf("") } ) is changed. So
2: In Code B, val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)} and val iconsVisible = remember {text.isNotBlank() } will be re-launched when the text is changed (val (text, setText) = remember { mutableStateOf("") }) , right?
Code A
#Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = text.isNotBlank()
Column {
Row( /* ... */ ) {
/* ... */
}
if (iconsVisible) {
AnimatedIconRow(icon, setIcon, Modifier.padding(top = 8.dp))
} else {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
Code B
#Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = remember {text.isNotBlank() } //I add remember
Column {
...
}
}
If you use remember as in Code B, iconsVisible will be calculated only once and the same value will be used across all recompositions and not get updates when text changes, which is not what we want here.
If you want to use remember here, you should pass text as a key to it, remember(text) { text.isNotBlank() }. But as this is not a time consuming calculation, you can just skip the remember block and use it as in Code A. The .isNotBlank() function will be invoked in every recomposition but that doesn't matter much here.
Related
I know the Column(){...} will be recomposition when either b1 or b2 is changed.
If I hope that Column(){...} can be re-composed only when b2 is changed and Column(){...} doesn't be recomposed when b1 is changed, how can I do?
#Composable
fun ScreenDetail(
mViewMode: SoundViewModel
) {
val b1=mViewMode.a1.collectAsState(initial = 0)
val b2=mViewMode.a2.collectAsState(initial = 0)
Column() {
Text(" ${b1.value} ${b2.value}")
Text(Calendar.getInstance().time.toSeconds())
}
}
fun Date.toSeconds():String{
return SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US).format(this)
}
class SoundViewModel(): ViewModel() {
var i = 0
val a1: Flow<Int> = flow {
while (true) {
emit(i++)
delay(1000)
}
}
val a2: Flow<Int> = flow {
while (true) {
emit(i)
delay(2000)
}
}
}
You need to create scopes if you need to have scoped recompositions. By scopes i mean creating a Composable that is not inline unlike Column, Row or Box. You can check answer and articles in this link.
Compose recomposes closest scope/function that States are read. If you read
Text(" ${b1.value} ${b2.value}")
your Column will be recomposed when any of these states changes. But as mentioned above even if you read any of the they should have changed because Column doesn't create a scope
#Composable
fun ScreenDetail2(
mViewMode: SoundViewModel
) {
val b1=mViewMode.a1.collectAsState(initial = 0)
val b2=mViewMode.a2.collectAsState(initial = 0)
Column(modifier= Modifier.background(getRandomColor()).fillMaxWidth()) {
Text("${b1.value}")
}
Column(modifier= Modifier.background(getRandomColor()).fillMaxWidth()) {
Text("${b2.value}")
}
}
But if you create a function such as
#Composable
private fun MyColumn(counter:Int){
Column(modifier= Modifier.background(getRandomColor()).fillMaxWidth()) {
Text("$counter")
}
}
you will be having scopes for each value you read
#Composable
fun ScreenDetail3(
mViewMode: SoundViewModel
) {
val b1=mViewMode.a1.collectAsState(initial = 0)
val b2=mViewMode.a2.collectAsState(initial = 0)
MyColumn(b1.value)
MyColumn(b2.value)
}
As you can see in the gif ScreenDetail2 recomposes each Column when b1 or b2 changes but ScreenDetail3 only recomposes respective scope of function. I changed delay time of b1 to 300 and b2 to 2000 to make recomposition easy to observe visually.
2 tolumns on top is from ScreenDetail2, and the bottom is from ScreenDetail3
Recomposition can be observer in many ways, i add 2
class Ref(var value: Int)
// Note the inline function below which ensures that this function is essentially
// copied at the call site to ensure that its logging only recompositions from the
// original call site.
#Composable
inline fun LogCompositions(msg: String) {
val ref = remember { Ref(0) }
SideEffect { ref.value++ }
println("$msg, recomposition: ${ref.value}")
}
or changing colors
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
The Code A is from the official sample code here.
I know that in order to preserve state across recompositions, remember the mutable state using remember.
I think that the code val extraPadding by animateDpAsState(...) should be val extraPadding by remember { animateDpAsState(...) }, is it right?
BTW, val extraPadding by remember { animateDpAsState(...) } will cause the error
Composable calls are not allowed inside the calculation parameter of inline fun remember(calculation: () -> TypeVariable(T)): TypeVariable(T)
Code A
#Composable
private fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState( //Should I add remember
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
No, you shouldn't. This function is marked with #Composable so it should be used directly in the view builder.
animateDpAsState will calculate its value depending on targetValue on each recomposition.
If you check it source code you'll see, that it uses remember inside, that's why it's marked with #Composable, and that's why you shouldn't bother about remembering some values manually.
For anyone that comes across this in the future, instead of attempting to remember the animateDpAsState it would be better to remember the expansion state. Because in this case we're trying to remember the state in an item of a list we use rememberSaveable:
var expanded by rememberSaveable { mutableStateOf(false) }
A very similar case of storing indexes across list items is shown in the official documentation here
For reasons that have to do with Jetpack Compose input modifiers consuming all MotionEvents, I find myself writing my own scroll routine for a Composable, of which I have access to the ScrollState.
I have figured out everything I need except flinging. I can't see how to apply performFling(initialVelocity) on a ScrollState. All I can find in the docs are ScrollState.scrollTo and ScrollState.scrollBy, which aren't so useful with flings, since the scroll destination or size is unknown.
I also can't find a ScrollState listener, similar to onScrollStateChanged(state: Int) in the old Android world, that fires when scrolling state changes.
Here is what I have in case somebody can point me in the right direction:
var lastY: Float? = null
var velocityTracker: VelocityTracker? = null
fun scroll(event: MotionEvent) {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
velocityTracker = VelocityTracker.obtain()
lastY = event.y
}
MotionEvent.ACTION_UP -> {
lastY = null
velocityTracker?.let {
it.computeCurrentVelocity(1000)
val initialVelocity = it.yVelocity
velocityTracker?.recycle()
coroutineScope.launch {
???? scrollState.PERFORMFLING?(initialVelocity) ????
AND THEN WHEN THE FLING IS FINISHED viewModel.scrollOffset = scrollState.value
}
}
}
else -> {
velocityTracker?.addMovement(event)
lastY?.let {
val scrollAmount = it - event.y
lastY = event.y
coroutineScope.launch {
scrollState.scrollBy(scrollAmount)
viewModel.scrollOffset = scrollState.value
}
}
}
}
}
You could try using a nestedScroll:
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return super.onPostFling(consumed, available)
}
override suspend fun onPreFling(available: Velocity): Velocity {
return super.onPreFling(available)
}
}
}
Column(modifier = Modifier
.verticalScroll(rememberScrollState())
.nestedScroll(nestedScrollConnection)) {
}
If this doesn't work, just search
cs.android.com
for theNestedScrollConnection and it should give you a hint on how to handle flinging in Compose. Maybe NestedScrollConnection is all you need since it provides support for scrolling as well. You probably can ditch your code and just use NestedScrollConnection. To see NestedScrollConnection in action, check out the demo:
https://github.com/JohannBlake/Jetmagic
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