I have to implement Google's "Place Autocomplete" on Jetpack Compose, but the problem is that once I get the list of places, I can't update the UI.
Going into more detail, the places received from the google API are stored in a MutableStateFlow <MutableList <String>> and the status is observed in a Composable function via: databaseViewModel.autocompletePlaces.collectAsState(). However, when a new item is added to the list, the Composable function is not re-compiled
Class that gets the places:
class AutocompleteRepository(private val placesClient: PlacesClient)
{
val autocompletePlaces = MutableStateFlow<MutableList<String>>(mutableListOf())
fun fetchPlaces(query: String) {
val token = AutocompleteSessionToken.newInstance()
val request = FindAutocompletePredictionsRequest.builder()
.setSessionToken(token)
.setCountry("IT")
.setQuery(query)
.build()
placesClient.findAutocompletePredictions(request).addOnSuccessListener {
response: FindAutocompletePredictionsResponse ->
autocompletePlaces.value.clear()
for (prediction in response.autocompletePredictions) {
autocompletePlaces.value.add(prediction.getPrimaryText(null).toString())
}
}
}
}
ViewModel:
class DatabaseViewModel(application: Application): AndroidViewModel(application) {
val autocompletePlaces: MutableStateFlow<MutableList<String>>
val autocompleteRepository: AutocompleteRepository
init {
Places.initialize(application, apiKey)
val placesClient = Places.createClient(application)
autocompleteRepository = AutocompleteRepository(placesClient)
autocompletePlaces = autocompleteRepository.autocompletePlaces
}
fun fetchPlaces(query: String)
{
autocompleteRepository.fetchPlaces(query)
}
}
Composable function:
#Composable
fun dropDownMenu(databaseViewModel: DatabaseViewModel) {
var placeList = databaseViewModel.autocompletePlaces.collectAsState()
//From here on I don't think it's important, but I'll put it in anyway:
var expanded by rememberSaveable { mutableStateOf(true) }
var placeName by rememberSaveable { mutableStateOf("") }
Column {
OutlinedTextField(value = placeName, onValueChange =
{ newText ->
placeName = newText
databaseViewModel.fetchPlaces(newText)
})
DropdownMenu(expanded = expanded,
onDismissRequest = { /*TODO*/ },
properties = PopupProperties(
focusable = false,
dismissOnBackPress = true,
dismissOnClickOutside = true)) {
placeList.value.forEach { item ->
DropdownMenuItem(text = { Text(text = item) },
onClick = {
placeName = item
expanded = false
})
}
}
}
}
EDIT:
solved by changing: MutableStateFlow<MutableList<String>>(mutableListOf()) to MutableStateFlow<List<String>>(listOf()), but I still can't understand what has changed, since the structure of the list was also changed in the previous code
class AutocompleteRepository(private val placesClient: PlacesClient)
{
val autocompletePlaces = MutableStateFlow<List<String>>(listOf()) //Changed
fun fetchPlaces(query: String) {
val token = AutocompleteSessionToken.newInstance()
val request = FindAutocompletePredictionsRequest.builder()
.setSessionToken(token)
.setCountry("IT")
.setQuery(query)
.build()
val temp = mutableListOf<String>()
placesClient.findAutocompletePredictions(request).addOnSuccessListener {
response: FindAutocompletePredictionsResponse ->
for (prediction in response.autocompletePredictions) {
temp.add(prediction.getPrimaryText(null).toString())
}
autocompletePlaces.value = temp //changed
}
}
}
In your old code you were just changing the MutableList instance inside the StateFlow by adding items to it. This does not trigger an update because it is still the same list (but with extra values in it).
With your new code you are changing the whole value of the StateFlow to a new list which triggers an update.
You can simplify your new code to something like:
autocompletePlaces.update {
it + response.autocompletePredictions.map { prediction ->
prediction.getPrimaryText(null).toString()
}
}
Related
My ViewModel
val names =
listOf(
"valeria",
"Daniela",
"Isabella",
"Liam",
"Noah",
"Jack",
"Oliver",
"Ava",
"Sophia",
"Amelia"
)
private val _namesList = MutableStateFlow(names)
val namesList = _namesList.asStateFlow()
fun getFilteredNames(state: MutableState<TextFieldValue>) {
viewModelScope.launch {
val searchedText = state.value.text
_namesList.value =
if (searchedText.isEmpty()) {
names
} else {
val resultList = ArrayList<String>()
for (item in names) {
if (item.lowercase(Locale.getDefault())
.contains(searchedText.lowercase(Locale.getDefault()))
) {
resultList.add(item)
}
}
Log.d("List: ", namesList.value.toString())
resultList
}
}
}
Recomposition doesn't happen for some reason.
val viewModel: MainViewModel = viewModel()
val names = viewModel.namesList.collectAsState()
LazyColumn(
modifier = Modifier
.fillMaxSize().background(MaterialTheme.colors.background)
) {
items(names.value.size) {
SearchListItem(names.value[it]) {}
}
}
Once I had the same question (you can see it and some other answers here). if shortly, flow is updated only when you emit the new object to it (with different hashcode than the last emitted item). and you are updating the same list and then trying to pass it to flow. if you will create the copy of the list and emit this copy, everything will work ok, as it is already another object
So I am not sure how are you using this callback to get more items. Simplified it would be like:
val mutableNamesList = MutableStateFlow(listOf<String>())
val namesList = mutableNamesList.asStateFlow()
#Composable
fun NamesList() {
val coroutineScope = rememberCoroutineScope()
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { newTextValue ->
text = newTextValue
coroutineScope.launch {
mutableNamesList.value = ( mutableNamesList.value + newTextValue)
}
})
val list = namesList.collectAsState().value
LazyColumn {
items(list) { item ->
Text(text = item)
}
}
}
Just, I used this import:
import androidx.compose.foundation.lazy.items
I faced the following problem:
There's the registration screen, which has several input fields on it. When a user enters something, the value is passed to ViewModel, set to screen state and passed back to the screen via StateFlow. From the composable, I'm observing this StateFlow. The problem is that Composable is not invalidated after emitting the new value to the Flow.
Here's the ViewModel code:
class RegistrationViewModel : BaseViewModel() {
private val screenData = CreateAccountRegistrationScreenData.init()
private val _screenDataFlow = MutableStateFlow(screenData)
internal val screenDataFlow = _screenDataFlow.asStateFlow()
internal fun updateFirstName(name: String) {
screenData.firstName = name
updateScreenData()
}
private fun updateScreenData() {
viewModelScope.launch {
_screenDataFlow.emit(screenData.copy())
}
println()
}
}
Here's the composable code:
#Composable
fun RegistrationScreen(navController: NavController, stepName: String) {
val focusManager = LocalFocusManager.current
val viewModel: RegistrationViewModel by rememberInstance()
val screenData by viewModel.screenDataFlow.collectAsState()
Scaffold {
ConstraintLayout(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(onTap = {
focusManager.clearFocus()
})
},
) {
val (
textTopLabel,
textBottomLabel,
tfFirstName,
tfLastName,
tfEmail,
tfPassword,
btnNext
) = createRefs()
...
RegistrationOutlinedTextField(
value = screenData.firstName ?: "",
constraintAsModifier = {
constrainAs(tfFirstName) {
top.linkTo(textBottomLabel.bottom, margin = Padding30)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
},
label = { Text("First Name", style = PoppinsNormalStyle14) },
leadingIcon = {
Image(
painter = painterResource(id = R.drawable.ic_user_login),
contentDescription = null
)
},
onValueChange = { viewModel.updateFirstName(it) }
)
}
}
Thanks in advance for any help
Your problem is that you're mutating your state, so the equals used in the flow always returns true.
Change this
internal fun updateFirstName(name: String) {
screenData.firstName = name
updateScreenData()
}
private fun updateScreenData() {
viewModelScope.launch {
_screenDataFlow.emit(screenData.copy())
}
println()
}
to
internal fun updateFirstName(name: String) {
viewModelScope.launch {
_screenDataFlow.emit(screenData.copy(firstName = name))
}
}
In your case your MutableStateFlow holds a link to a mutable value, that's why when you pass the value which hashcode(which is calculated on all field values) is identical, the flow doesn't update the value.
Check out Why is immutability important in functional programming?
data class is a nice tool to be used, which will provide you all copy out of the box, but you should emit using var and only use val for your fields to avoid mistakes.
Also, with MutableStateFlow you always have access to value, so you don't need to store it in a separate variable:
class RegistrationViewModel : BaseViewModel() {
private val _screenDataFlow = MutableStateFlow(CreateAccountRegistrationScreenData())
internal val screenDataFlow = _screenDataFlow.asStateFlow()
internal fun updateFirstName(name: String) {
_screenDataFlow.update { screenData ->
screenData.copy(firstName = name)
}
}
}
i am working on compose project. I have simple login page. After i click login button, loginState is set in viewmodel. The problem is when i set loginState after service call, my composable recomposed itself multiple times. Thus, navcontroller navigates multiple times. I don't understand the issue. Thanks for helping.
My composable :
#Composable
fun LoginScreen(
navController: NavController,
viewModel: LoginViewModel = hiltViewModel()
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly
) {
val email by viewModel.email
val password by viewModel.password
val enabled by viewModel.enabled
if (viewModel.loginState.value) {
navController.navigate(Screen.HomeScreen.route) {
popUpTo(Screen.LoginScreen.route) {
inclusive = true
}
}
}
LoginHeader()
LoginForm(
email = email,
password = password,
onEmailChange = { viewModel.onEmailChange(it) },
onPasswordChange = { viewModel.onPasswordChange(it) }
)
LoginFooter(
enabled,
onLoginClick = {
viewModel.login()
},
onRegisterClick = {
navController.navigate(Screen.RegisterScreen.route)
}
)
}
ViewModel Class:
#HiltViewModel
class LoginViewModel #Inject constructor(
private val loginRepository: LoginRepository,
) : BaseViewModel() {
val email = mutableStateOf(EMPTY)
val password = mutableStateOf(EMPTY)
val enabled = mutableStateOf(false)
val loginState = mutableStateOf(false)
fun onEmailChange(email: String) {
this.email.value = email
checkIfInputsValid()
}
fun onPasswordChange(password: String) {
this.password.value = password
checkIfInputsValid()
}
private fun checkIfInputsValid() {
enabled.value =
Validator.isEmailValid(email.value) && Validator.isPasswordValid(password.value)
}
fun login() = viewModelScope.launch {
val response = loginRepository.login(LoginRequest(email.value, password.value))
loginRepository.saveSession(response)
loginState.value = response.success ?: false
}
}
You should not cause side effects or change the state directly from the composable builder, because this will be performed on each recomposition.
Instead you can use side effects. In your case, LaunchedEffect can be used.
if (viewModel.loginState.value) {
LaunchedEffect(Unit) {
navController.navigate(Screen.HomeScreen.route) {
popUpTo(Screen.LoginScreen.route) {
inclusive = true
}
}
}
}
But I think that much better solution is not to listen for change of loginState, but to make login a suspend function, wait it to finish and then perform navigation. You can get a coroutine scope which will be bind to your composable with rememberCoroutineScope. It can look like this:
suspend fun login() : Boolean {
val response = loginRepository.login(LoginRequest(email.value, password.value))
loginRepository.saveSession(response)
return response.success ?: false
}
Also check out Google engineer thoughts about why you shouldn't pass NavController as a parameter in this answer (As per the Testing guide for Navigation Compose ...)
So your view after updates will look like:
#Composable
fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel(),
onLoggedIn: () -> Unit,
onRegister: () -> Unit,
) {
// ...
val scope = rememberCoroutineScope()
LoginFooter(
enabled,
onLoginClick = {
scope.launch {
if (viewModel.login()) {
onLoggedIn()
}
}
},
onRegisterClick = onRegister
)
// ...
}
And your navigation route:
composable(route = "login") {
LoginScreen(
onLoggedIn = {
navController.navigate(Screen.HomeScreen.route) {
popUpTo(Screen.LoginScreen.route) {
inclusive = true
}
}
},
onRegister = {
navController.navigate(Screen.RegisterScreen.route)
}
)
}
I have a composable function that looks something like this:
#Composable
fun listScreen(context: Context, owner: ViewModelStoreOwner) {
val repository = xRepository(getAppDatabase(context).xDao()
val listData by repository.readAllData.observeAsState(emptyList())
// repository.readAllData returns LiveData<List<xEntity>>
// listData is a List<xEntity>
LazyColumn(){
items(listData.size) {
Card {
Text(listData[it].name)
Text(listData[it].hoursLeft.toString())
Button(onClick = {updateInDatabase(owner, name = listData[it], hoursLeft = 12)}) {...}
}
}
}
}
fun updateInDatabase(owner: ViewModelStoreOwner, name: String, hoursLeft: Int) {
val xViewModel....
val newEntity = xEntity(name=name, hoursLeft = Int)
xViewModel.update(newEntity)
}
and as you propably can guess, the LazyColumn doesn't refresh after modification of entity, is there a way to update listData after every update of entity?
edit:
class xRepository(private val xDatabaseDao) {
val readAllData: LiveData<List<xEntity>> = xDatabaseDao.getallXinfo()
...
suspend fun updatePlant(x: xEntity) {
plantzDao.updateX(x)
}
}
interface xDatabaseDao {
#Query("SELECT * FROM xInfo ORDER BY id DESC")
fun getAllXInfo(): LiveData<List<xEntity>>
....
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun updateX(x: xEntity?)
}
modification of entity:
fun updatePlantInDatabase(owner: ViewModelStoreOwner, name: String, waterAtHour: Int, selectedDays: ArrayList<Int>) {
val xViewModel: xViewModel = ViewModelProvider(owner).get(xViewModel::class.java)
val new = xEntity(name = name, waterAtHour = waterAtHour, selectedDays = selectedDays)
xViewModel.updatePlant(new)
}
I use mutableStateOf to wrap fields that need to recomposed. Such as
class TestColumnEntity(
val id: String,
title: String = ""
){
var title: String by mutableStateOf(title)
}
View:
setContent {
val mData = mutableStateListOf(
TestColumnEntity("id_0").apply { title = "cnm"},
TestColumnEntity("id_1").apply { title = "cnm"},
TestColumnEntity("id_2").apply { title = "cnm"},
TestColumnEntity("id_3").apply { title = "cnm"},
TestColumnEntity("id_4").apply { title = "cnm"},
TestColumnEntity("id_5").apply { title = "cnm"},
)
Column {
Button(onClick = {
mData.add(TestColumnEntity("id_${Random.nextInt(100) + 6}").apply { title = "ccnm" })
}) {
Text(text = "add data")
}
Button(onClick = {
mData[1].title = "test_${Random.nextInt(100)}"
}) {
Text(text = "update data")
}
TestLazyColumn(data = mData, key = {index, item ->
item.id
}) {
Text(text = it.title)
}
}
}
It works in my testcase
If you want to update lazy column (say recompose in jetpack compose) so use side effects.
Put list getting function in side effect (Launch Effect or other side effects) when list is change side effect automatic recompose your function and show updated list.
I have a problem for now in JetpackCompose.
The problem is, when I'm collecting the Data from a flow, the value is getting fetched from firebase like there is a listener and the data's changing everytime. But tthat's not that.
I don't know what is the real problem!
FirebaseSrcNav
suspend fun getName(uid: String): Flow<Resource.Success<Any?>> = flow {
val query = userCollection.document(uid)
val snapshot = query.get().await().get("username")
emit(Resource.success(snapshot))
}
NavRepository
suspend fun getName(uid: String) = firebase.getName(uid)
HomeViewModel
fun getName(uid: String): MutableStateFlow<Any?> {
val name = MutableStateFlow<Any?>(null)
viewModelScope.launch {
navRepository.getName(uid).collect { nameState ->
when (nameState) {
is Resource.Success -> {
name.value = nameState.data
//_posts.value = state.data
loading.value = false
}
is Resource.Failure<*> -> {
Log.e(nameState.throwable, nameState.throwable)
}
}
}
}
return name
}
The probleme is in HomeScreen I think, when I'm calling the collectasState().value.
HomeScreen
val state = rememberLazyListState()
LazyColumn(
state = state,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(post) { post ->
//val difference = homeViewModel.getDateTime(homeViewModel.getTimestamp())
val date = homeViewModel.getDateTime(post.timeStamp!!)
val name = homeViewModel.getName(post.postAuthor_id.toString()).collectAsState().value
QuestionCard(
name = name.toString(),
date = date!!,
image = "",
text = post.postText!!,
like = 0,
response = 0,
topic = post.topic!!
)
}
}
I can't post video but if you need an image, imagine a textField where the test is alternating between "null" and "MyName" every 0.005 second.
Check official documentation.
https://developer.android.com/kotlin/flow
Flow is asynchronous
On viewModel
private val _name = MutableStateFlow<String>("")
val name: StateFlow<String>
get() = _name
fun getName(uid: String) {
viewModelScope.launch {
//asyn call
navRepository.getName(uid).collect { nameState ->
when (nameState) {
is Resource.Success -> {
name.value = nameState.data
}
is Resource.Failure<*> -> {
//manager error
Log.e(nameState.throwable, nameState.throwable)
}
}
}
}
}
on your view
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
viewModel.name.collect { name -> handlename
}
}
}