I am trying to store the data in mutableStateOf() to view it in my compose function, I receive the response in my variable and assign it to State<List?> but in my compose function I recieve no data at all, yet the init block in viewmodel when I debug showes that my data is recieved?
I dont understand how MutablStates works and why it doesn't function like Live data? and if it doesn't functions like LiveData what is its alternative in JetPack compose
Here is my code in my Viewmodel
private var _categoryItems = mutableStateOf<List<Data>?>(emptyList())
val categoryItems : State<List<Data>?> = _categoryItems
init {
val service = RetrofitInstance.getRetrofitInstance().create(IGetBusinessCategory::class.java)
viewModelScope.launch {
var logText = ""
logText = service.getBusinessCategory().toString()
val categoryItems = service.getBusinessCategory().body()!!.data
Log.v("CategoryItems", logText)
_categoryItems = mutableStateOf(categoryItems)
}
}
}
and here how I try to get the data in my Compose Screen
val businessCategoryModel = BusiniessCategoryViewModel()
val listOfCategories by rememberSaveable { businessCategoryModel.categoryItems }
Yet when I debug I recieve no items at all in listOfCategories while its recieved in _categoryItems in the viewmodel, so how can I get the functionality of livedata with states in compose? is this even possiable?
Instead of:
_categoryItems = mutableStateOf(categoryItems)
Try this:
_categoryItems.value = categoryItems
Also, instead of:
val listOfCategories by rememberSaveable { businessCategoryModel.categoryItems }
Try this:
val listOfCategories by remember { mutableStateOf(businessCategoryModel.categoryItems) }
Related
I have a case where I have a ViewModel that emits scroll values to a StateFlow. Now, my Composable view should scroll to the most recent value, but I can't figure out how to achieve that.
My view model looks something like this:
class MyViewModel : ViewModel() {
private val scrollFlow = MutableStateFlow<Int>(0)
fun getScrollFlow(): StateFlow<Int> = scrollFlow.asStateFlow()
}
And my view is like this:
#Composable
fun MyScrollingView() {
val viewModel = viewModel()
val scroll by viewModel.getScrollFlow().collectAsState()
val scrollState = rememberScrollState(0)
Column(modifier = Modifier.verticalScroll(scrollState)) {
// Content here
}
}
The thing here is, how do I make the scroll state to react to the values coming from the view model?
There are many way to achieve that, for example:
val scrollState = rememberScrollState(0)
rememberCoroutineScope().launch {
viewModel.getScrollFlow()
.flowOn(Dispatchers.Default)
.onEach{scrollVal ->
scrollState.animateScrollTo(scrollVal)
}.collect()
}
Actually I'm afraid that code could be launched on every recomposition and is not ideal so maybe:
remember{
viewModel
.getScrollFlow()
.onEach{scrollVal ->
scrollState.animateScrollTo(scrollVal)
}
}.collectAsState(0, Dispatchers.Default)
or
val scroll by viewModel.getScrollFlow().collectAsState(0, Dispatchers.Default)
LaunchedEffect(scroll){
crollState.animateScrollTo(scroll)
}
You can try doing it inside a LaunchedEffect and use the scrollFlow value as its key, every time the scrollFlow emits new value, the LaunchedEffect will trigger its block.
#Composable
fun MyScrollingView() {
val viewModel = viewModel()
val scroll by viewModel.getScrollFlow().collectAsState()
val scrollState = rememberScrollState(0)
Column(modifier = Modifier.verticalScroll(scrollState)) {
// Content here
}
LaunchedEffect(scroll) {
scrollState.animateScrollTo(0)
//or
// scrollState.scrollTo(0)
}
}
I'm just not sure if this would work in your case especially having this statement inside your composable though.
val viewModel = viewModel()
I am quite new to Jetpack compose and have an issue that my list is not recomposing when a property of an object in the list changes. In my composable I get a list of available appointments from my view model and it is collected as a state.
// AppointmentsScreen.kt
#Composable
internal fun AppointmentScreen(
navController: NavHostController
) {
val appointmentsViewModel = hiltViewModel<AppointmentViewModel>()
val availableAppointments= appointmentsViewModel.appointmentList.collectAsState()
AppointmentContent(appointments = availableAppointments, navController = navController)
}
In my view model I get the data from a dummy repository which returns a flow.
// AppointmentViewModel.kt
private val _appointmentList = MutableStateFlow(emptyList<Appointment>())
val appointmentList : StateFlow<List<Appointment>> = _appointmentList.asStateFlow()
init {
getAppointmentsFromRepository()
}
// Get the data from the dummy repository
private fun getAppointmentsFromRepository() {
viewModelScope.launch(Dispatchers.IO) {
dummyRepository.getAllAppointments()
.distinctUntilChanged()
.collect { listOfAppointments ->
if (listOfAppointments.isNullOrEmpty()) {
Log.d(TAG, "Init: Empty Appointment List")
} else {
_appointmentList.value = listOfAppointments
}
}
}
}
// dummy function for demonstration, this is called from a UI button
fun setAllStatesToPaused() {
dummyRepository.setSatesInAllObjects(AppointmentState.Finished)
// Get the new data
getAppointmentsFromRepository()
}
Here is the data class for appointments
// Appointment data class
data class Appointment(
val uuid: String,
var state: AppointmentState = AppointmentState.NotStarted,
val title: String,
val timeStart: LocalTime,
val estimatedDuration: Duration? = null,
val timeEnd: LocalTime? = null
)
My question: If a property of one of the appointment objects (in the view models variable appointmentList) changes then there is no recomposition. I guess it is because the objects are still the same and only the properties have changed. What do I have to do that the if one of the properties changes also a recomposition of the screen is fired?
For example if you have realtime app that display stocks/shares with share prices then you will probably also have a list with stock objects and the share price updates every few seconds. The share price is a property of the stock object so this quite a similiar situation.
I'm having an issue trying to display the data saved in my DataStore on startup in Jetpack Compose.
I have a data store set using protocol buffers to serialize the data. I create the datastore
val Context.networkSettingsDataStore: DataStore<NetworkSettings> by dataStore(
fileName = "network_settings.pb",
serializer = NetworkSettingsSerializer
)
and turn it into a livedata object in the view model
val networkSettingsLive = dataStore.data.catch { e ->
if (e is IOException) { // 2
emit(NetworkSettings.getDefaultInstance())
} else {
throw e
}
}.asLiveData()
Then in my #Composable I try observing this data asState
#Composable
fun mycomposable(viewModel: MyViewModel) {
val networkSettings by viewModel.networkSettingsLive.observeAsState(initial = NetworkSettings.getDefaultInstance())
val address by remember { mutableStateOf(networkSettings.address) }
Text(text = address)
}
I've confirmed that the data is in the datastore, and saving properly. I've put some print statements in the composible and the data from the datastore makes it, eventually, but never actually displays in my view. I want to say I'm not properly setting my data as Stateful the right way, but I think it could also be not reading from the data store the right way.
Is there a display the data from the datastore in the composable, while displaying the initial data on start up as well as live changes?
I've figured it out.
What I had to do is define the state variables in the composable, and later set them via a state controlled variable in the view model, then set that variable with what's in the dataStore sometime after initilization.
class MyActivity(): Activity {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
MainScope.launch {
val networkSettings = viewModel.networkSettingsFlow.firstOrNull()
if (networkSettings != null) {
viewModel.mutableNetworkSettings.value = networkSettings
}
}
}
}
class MyViewModel(): ViewModel {
val networkSettingsFlow = dataStore.data
val mutableNetworkSettings = mutableStateOf(NetworkSettings.getInstance()
}
#Composable
fun NetworkSettings(viewModel: MyViewModel) {
val networkSettings by viewModel.mutableNetworkSettings
var address by remember { mutableStateOf(networkSettings.address) }
address = networkSettings.address
Text(text = address)
}
How can I fetch json from url and add it's data to a Text Composable in Jetpack Compose
Here is json file
https://jsonplaceholder.typicode.com/posts
#Composable
fun Api(){
val queue = Volley.newRequestQueue(LocalContext.current)
val url = "https://jsonplaceholder.typicode.com/posts"
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET, url,null,
{ response ->
val title = response.getString("title")
print(title)
},
{ error ->
print(error.localizedMessage)
})
queue.add(jsonObjectRequest)
}
Just get the data however you want to inside a viewmodel. Then, store it in a variable, like var data by mutableStateOf("")
Then access this variable through the viewmodel from your text Composable. Updating this variable like a normal string will trigger recompositions
EDIT BASED ON THE COMMENT BELOW:-
Although it is not necessary to store it in a viewmodel, it is the recommended best practice. You can also store the state inside your normal activity class or even the Composable using remember(not recommended for important state storage)
However, by viewmodel, I just meant,
class mViewModel: ViewModel(){
var data by mutableStateOf("")
private set //only viewmodel can modify values
fun onLoadData(){
data = //json extraction logic
}
fun onDataChange(newData: String){
data = newData
}
}
Then, in your activity,
class mActiviry: AppCompatActivity(){
val vm by viewmodels<mViewModel>() //See docs for better approaches of initialisation
//...
setContent {
Text(vm.data)
}
}
Done
Edit:-
Alternately, ditch the onDataLoad()
class mViewModel: ViewModel(){
var data by mutableStateOf("")
private set //only viewmodel can modify values
init{
data = // code from the "Api" method in your question
}
fun onDataChange(newData: String){
data = newData
}
}
I have a LiveData property for login form state like this
private val _authFormState = MutableLiveData<AuthFormState>(AuthFormState())
val authFormState: LiveData<AuthFormState>
get() =_authFormState
The AuthFormState data class has child data objects for each field
data class AuthFormState (
var email: FieldState = FieldState(),
var password: FieldState = FieldState()
)
and the FieldState class looks like so
data class FieldState(
var error: Int? = null,
var isValid: Boolean = false
)
When user types in some value into a field the respective FieldState object gets updated and assigned to the parent AuthFormState object
fun validateEmail(text: String) {
_authFormState.value!!.email = //validation result
}
The problem is that the authFormState observer is not notified in this case.
Is it possible to trigger the notification programically?
Maybe you can do:
fun validateEmail(text: String) {
val newO = _authFormState.value!!
newO.email = //validation result
_authFormState.setValue(newO)
}
You have to set the value to itself, like this: _authFormState.value = _authFormState.value to trigger the refresh. You could write an extension method to make this cleaner:
fun <T> MutableLiveData<T>.notifyValueModified() {
value = value
}
For such a simple data class, I would recommend immutability to avoid issues like this altogether (replaces all those vars with vals). Replace validateEmail() with something like this:
fun validateEmail(email: String) = //some modified version of email
When validating fields, you can construct a new data object and set it to the live data.
fun validateFields() = _authFormState.value?.let {
_authFormState.value = AuthFormState(
validateEmail(it.email),
validatePassword(it.password)
)
}