i was checking my debug and i see my ViewModel or screen run multi time
my viewModel
#HiltViewModel
class DownloadViewModel #Inject constructor(
private val repository: DataRepository
): ViewModel() {
val downloadList: LiveData<List<DownloadModel>> = repository.getDownload().asLiveData()
my screen
#Composable
fun TetsScreen(dm : DownloadViewModel){
Log.e("is running", "true")
val downloads by dm.downloadList.observeAsState()
Column(modifier = Modifier
.background(color = Color.White)
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
if (downloads.isNullOrEmpty()){
//to do
}else{
Log.e("its", "work")
}
}
my NavHost
val navController = rememberNavController()
NavHost(navController = navController, startDestination = AppScreens.MainScreen.name){
composable(AppScreens.TestScreen.name){
TetsScreen(downloadViewModel)
}
composable(AppScreens.MainScreen.name){
MainScreen(navController = navController, userViewModel, profileViewModel, downloadViewModel)
}
for starting new Screen
LaunchedEffect(Unit){
navController.navigate(AppScreens.TestScreen.name) {
popUpTo(AppScreens.MainScreen.name) { inclusive = true}
}
}
and my debug
E/is running: true
E/is running: true
E/is running: true
E/its: work
E/is running: true
E/its: work
E/its: work
but when i remove
val downloads = dm.downloadList.observeAsState()
everything will be normal
i try start screen without LaunchedEffect but nothing change
please if you can help me
It's not actually your view model, as you're collecting data from LiveData, everytime you get an update (regardless of it's content), compose detects it and gets refreshed on every new observed data change, that's why you see your Log on every re-composition as "E/is running: true" until some data it's succesfully loaded and finally shows you a "E/is running: true" followed by a Log of "E/its: work".
Related
I am trying to build a simple listing app and to fetch data from url I use retrofit2. And then, I store in MutableLiveData<Resource> object. How can I list retrofit2 results in LazyColumn?
My composable:
#ExperimentalAnimationApi
#Composable
fun CarsScreen(
viewModel: CarListViewModel = hiltViewModel()
){
viewModel.getCarsFromAPI()
val carsLiveData = viewModel.carsLiveData.observeAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
){
GreetingSection()
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
itemsIndexed( ){
//TODO
}
}
}
Viewmodel:
#HiltViewModel
class CarListViewModel #Inject constructor(
private val carRepository: CarRepository
): ViewModel() {
val carsLiveData:MutableLiveData<Resource<CarsResponse>> = MutableLiveData()
fun getCarsFromAPI() = viewModelScope.launch {
carsLiveData.postValue(Resource.Loading())
val response = carRepository.getCarsFromAPI()
carsLiveData.postValue(handleCarResponse(response))
}
private fun handleCarResponse(response: Response<CarsResponse>) : Resource<CarsResponse> {
if(response.isSuccessful){
response.body()?.let{resultResponse ->
return Resource.Success(resultResponse)
}
}
return Resource.Error(response.message())
}
}
observeAsState doesn't return a LiveData; it returns the data contained within the LiveData that you're observing.
Whenever that LiveData's value changes, recomposition is triggered, and you'll get a new value.
Change the name of property carsLiveData to just cars. You can use that directly as the items in your LazyColumn.
One other note - you're calling viewModel.getCarsFromAPI() inside the CarsScreen composable every time it's recomposed. You probably don't want to do that.
If you only want to get the list once, you could use a LaunchedEffect inside CarsScreen, something like:
// Copyright 2023 Google LLC.
// SPDX-License-Identifier: Apache-2.0
#Composable
fun CarsScreen(...) {
LaunchedEffect(Unit) {
viewModel.getCarsFromAPI()
}
...
}
If you want to update that list, pass some state into LaunchedEffect instead of Unit - whenever that state changes, the LaunchedEffect will be canceled (if currently running) and restarted.
I'm trying to show a List of Items in my Android App. I'm using Jetpack Compose, Flows and RoomDB.
When launching the Activity all Items are shown without any problems, the Flow get's items collected and they are displayed.
But when I change some properties of the Item in the Database, the changes are not displayed. In my case I change the item to deleted, but it's still displayed as not deleted.
When I look at the Database Inspector, the value is changed in the database and set to deleted.
When I log collecting the flow, the change is getting emitted (It says the Item is set to deleted)
But Jetpack Compose is not recomposing the change.
If I delete an element from / add an element to the List (in the DB) the UI gets updated and recomposed.
So I can only assume that the problem must lie in the recomposition or handling of the flow.
Here my Code:
My Activity:
#AndroidEntryPoint
class StockTakingHistoryActivity : ComponentActivity() {
private val viewModel: StockTakingHistoryViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.stockList = ...
setContent {
LaunchedEffect(Unit) {
viewModel.getStockListItems(viewModel.stockList!!.uuid)
}
Surface(color = MaterialTheme.colors.background) {
Content(viewModel.stockListItems)
}
}
}
}
...
#Composable
private fun Content(items: List<StockListItem>) {
...
LazyColumn {
items(items) { item ->
HistoryItem(stockListItem = item)
}
}
}
}
...
#Composable
private fun HistoryItem(stockListItem: StockListItem) {
...
Text(text = stockListItem.deleted)
...
Button(onClick = {
viewModel.deleteItem(stockListItem)
}) {
Text(text = "Set to deleted!")
}
}
}
My ViewModel:
var stockListItems by mutableStateOf(emptyList<StockListItem>())
fun getStockListItems(uuid: String) {
viewModelScope.launch {
stockListItemRepository.findByUUID(uuid).collect { items ->
Log.d("StockTakingHistoryViewModel", "items changed! ${items.map { it.deleted }}")
stockListItems = items
}
}
}
fun deleteItem(stockListItem: StockListItem) {
viewModelScope.launch(Dispatchers.IO) {
stockListItemRepo.update(item.copy(deleted = true);
}
}
The Repository:
fun findByUUID(uuid: String): Flow<List<StockListItem>> {
return dao.findByUUID(uuid)
}
The Dao behind the Repository Request:
#Query("select * from StockListItem where stockListUUID = :uuid order by createdAt desc limit 30")
fun findByUUID(uuid: String): Flow<List<StockListItem>>
I would be very happy if someone could help me! Thank you!
Considering you can collect a flow as state (via collectAsState) I'd consider going that route for getting the list rather than calling collect in the viewModel and updating the stockListItems as there are fewer moving parts for things to go wrong.
For example something like the following:
setContent {
val stockListItems = viewModel.getStockListItemsFlow(uuid).collectAsState(initial = emptyList())
Surface(color = MaterialTheme.colors.background) {
Content(stockListItems)
}
}
Found the Problem: The equals() method of StockListItem only compared the primary key.
I've recently started my first project using Jetpack Compose (with minimal Android dev experience).
For checking performance, I logged each call to any function / composable, with some unexpected behavior at startup or orientation change (but without further interacting with the app):
I do understand oncreate / super being called (again, in case of orientation change), but why is it that setContent {} is being called twice?
override fun onCreate(savedInstanceState: Bundle?) {
Log.v(tag, "oncreate")
super.onCreate(savedInstanceState)
Log.v(tag, "oncreatesuper")
setContent {
Log.v(tag, "setting content")
Content()
}
}
and then
#Composable
private fun Content() {
val arrayOfNodes = rememberSaveable { mutableListOf<Wurzel>() }
val toggleSizeInputDialog = rememberSaveable { mutableStateOf(false) }
(...some more...)
val currentConfig = LocalConfiguration.current
val title = stringResource(id = R.string.title)
Log.v(tag, "recomposing content")
MyTheme {
when (currentConfig.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
Scaffold(...........)
My project is far to small to see any perfomance issues, however I'd like to find out the reason for this behavior for future reference or whether I severely misunderstood the compose architecture.
setComponent() is not being called twice, but the composable function you pass in it is. It just seems like some event causes recomposition of the content composable.
My app consists of the home screen and on this screen, there is a button when users click on it they navigate to the login bottom sheet.
I am going to use this login bottom sheet elsewhere in the app so I prefer to make it a separate screen and navigate from home to login.
It is desirable to show the home screen as the background for the login screen. I mean the login bottom sheet's main content should be empty and transparent in order to see the home screen as the background. But instead of the home screen for background, the white background shows up.
Here are my codes:
LoginScreen:
#Composable
fun LoginScreen(
loginViewModel: LoginViewModel = hiltViewModel()
) {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
LoginContent()
},
sheetPeekHeight = 400.dp,
sheetShape = RoundedCornerShape(topEnd = 52.dp, topStart = 52.dp),
backgroundColor = Color.Transparent
) {
Box(modifier = Modifier.fillMaxSize().background(color = Color.Transparent)) {
}
}
}
HomeScreen:
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = hiltViewModel(),
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.White)
) {
somecontent
...
...
...
Button(onClick = {
viewModel.navigate(
LoginDestination.route()
)
}) {
Text("Go to the login screen")
}
}
}
I use navigation like this:
fun interface NavigationDestination {
fun route(): String
val arguments: List<NamedNavArgument>
get() = emptyList()
val deepLinks: List<NavDeepLink>
get() = emptyList()
}
and then Login destination overrides it:
object LoginDestination : NavigationDestination {
override fun route(): String = "login"
}
and here is the implementation of the navigator:
#Singleton
internal class ClientNavigatorImpl #Inject constructor() : ClientNavigator {
private val navigationEvents = Channel<NavigatorEvent>()
override val destinations = navigationEvents.receiveAsFlow()
override fun navigateUp(): Boolean =
navigationEvents.trySend(NavigatorEvent.NavigateUp).isSuccess
override fun popBackStack(): Boolean =
navigationEvents.trySend(NavigatorEvent.PopBackStack).isSuccess
override fun navigate(route: String, builder: NavOptionsBuilder.() -> Unit): Boolean =
navigationEvents.trySend(NavigatorEvent.Directions(route, builder)).isSuccess
}
and the navigator event is:
sealed class NavigatorEvent {
object NavigateUp : NavigatorEvent()
object PopBackStack : NavigatorEvent()
class Directions(
val destination: String,
val builder: NavOptionsBuilder.() -> Unit
) : NavigatorEvent()
}
the way you are trying to show the LoginScreen won't work as you expected because when you navigate to LoginScreen it's like opening a new Screen, HomeScreen is then added to the backstack and not shown behind your LoginScreen. To make it work, try like this:
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = hiltViewModel(),
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.White)
) {
Button(onClick = {
//TODO: Add functionality
}) {
Text("Go to the login screen")
}
}
}
And change the LoginScreen parameters that you can give it a Composable:
#Composable
fun LoginScreen(
loginViewModel: LoginViewModel = hiltViewModel(),
screen: #Composable (() -> Unit)
) {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
//The Login Content needs to be here
*EDIT*
BackHandler(enabled = true) {
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
*EDIT*
},
sheetPeekHeight = 400.dp,
sheetShape = RoundedCornerShape(topEnd = 52.dp, topStart = 52.dp),
backgroundColor = Color.Transparent
) {
screen() //Adds the content which is shown on the Screen behind bottomsheet
}
}
And then use it somehow like this:
LoginScreen( /*YourLoginViewModel*/) {
HomeScreen(Modifier, /*YourHomeScreenModel*/){
}
}
Now your bottom sheet is shown all the time, to hide it you need to work with the BottomSheetState collapsed/expanded and the sheetPeekHeight = 400.dp, which you need to set to 0 that the sheet is hidden completely at first
In the end you need to implement that the BottomSheetState changes on the ButtonClick where you navigated to the Screen in your first attempt
Edit:
Also don't use backgroundColor. To change the bottomSheets Background you need to use sheetBackgroundColor = Color.Transparent
helpinghand You are right about handling the device back button. But now when the user presses the back button only the Login screen's sheet content collapse and the main content of the login screen which is the main content of the home screen remains. In order to it works I don't need the composable callback screen as a parameter for the login screen function and instead, I replace it with another callback like (callback: () -> Unit) and whenever want to get rid of login screen just invoke it in the login screen (for example when clicking outside the bottom sheet or collapsing it) and then in the home screen create a boolean mutable state for detecting when it needs to show the login screen and so in the trailing lambda make the state false.
I have read some sample codes for learning Compose.
I find many sample projects use Code A to create a StateFlow in view model, then convert it to State in #Composable function, the UI will be updated automatically when drawerOpen is changed.
1: I think both Code B and Code C can do the same thing, right? Why does many projects seldom to use them?
2: Is Code A a good way ?
3: I needn't to add rememberSaveable for variable drawerOpen in #Composable fun myRoute(...) because view model will store data, right?
Code A
class MainViewModel : ViewModel() {
private val _drawerShouldBeOpened = MutableStateFlow(false)
val drawerShouldBeOpened: StateFlow<Boolean> = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen by MainViewModel.drawerShouldBeOpened.collectAsState() //Do I need add rememberSaveable ?
...
}
Code B
class MainViewModel : ViewModel() {
private var _drawerShouldBeOpened = mutableStateOf(false)
val drawerShouldBeOpened: State<Boolean> = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen = MainViewModel.drawerShouldBeOpened //Do I need add rememberSaveable ?
...
}
Code C
class MainViewModel : ViewModel() {
private var _drawerShouldBeOpened = false
val drawerShouldBeOpened: Boolean = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen = rememberSaveable { mutableStateOf(MainViewModel.drawerShouldBeOpened)) //Can I remove rememberSaveable ?
}
There are multiple questions here.
Let me answer whatever is possible.
1. Where should you use remember / rememberSaveable? (Code A, B, or C)
Only in code C it is required.
(No issues in using in code A and B as well, but no advantages there)
Reason,
In code A and B - the state is maintained in the view model. Hence the value survives recomposition.
But in code C, the state is created and maintained inside the composable. Hence remember is required for the value to survive recomposition.
More details in Docs
2. Why Code C is not used much?
Composable recomposition happens whenever there is a change in state, not the value.
Given below is a simple example to demonstrate the same.
class ToggleViewModel : ViewModel() {
private val _enabledStateFlow = MutableStateFlow(false)
val enabledStateFlow: StateFlow<Boolean> = _enabledStateFlow
private val _enabledState = mutableStateOf(false)
val enabledState: State<Boolean> = _enabledState
private var _enabled = false
val enabled: Boolean = _enabled
fun setEnabledStateFlow(isEnabled: Boolean) {
_enabledStateFlow.value = isEnabled
}
fun setEnabledState(isEnabled: Boolean) {
_enabledState.value = isEnabled
}
fun setEnabled(isEnabled: Boolean) {
_enabled = isEnabled
}
}
#Composable
fun BooleanToggle(
viewmodel: ToggleViewModel = ToggleViewModel(),
) {
val enabledStateFlow by viewmodel.enabledStateFlow.collectAsState()
val enabledState by viewmodel.enabledState
val enabled by rememberSaveable {
mutableStateOf(viewmodel.enabled)
}
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabledStateFlow) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabledStateFlow(!enabledStateFlow) }) {
Text("Toggle State Flow")
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabledState) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabledState(!enabledState) }) {
Text("Toggle State")
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabled) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabled(!enabled) }) {
Text("Toggle Value")
}
}
}
}
You can see that the third text will NOT update on clicking the button.
The reason is that the mutable state inside the composable was created using an initial value from the view model data. But further updates to that data will not be reflected in the composable.
To get updates, we have to use reactive data like Flow, LiveData, State, and their variants.
3. Using StateFlow vs State.
From the docs, you can see that compose supports Flow, LiveData and RxJava.
You can see in the usage that we are using collectAsState() for StateFlow.
The method converts StateFlow to State. So both can be used.
Use Flow if the layers beyond ViewModel (like repo) are the data sources and they use Flow data type.
Else MutableState should be fine.