I have a funny one
class RoleState(
var roles: List<String> = emptyList(),
var selectedRoles: List<String> = emptyList()
)
in my ViewModel I have
var rolesState by mutableStateOf(RoleState())
Below
rolesState = rolesState.copy(
selectedRoles = result.data ?: kotlin.run {
_eventFlow.emit(value =UiEvent.ShowSnackbar(uiText = UiText.errorUnknown()))
return#launch
}
)
Normally .copy would work, but now emits Unresolved reference: copy
Only reason I can think of is that the State class needs to be primatives and Data Classes only?
RoleState has to be a data class in order to get the compiler to create a copy function. If you don't want to make it a data class you have to provide/implement the copy function on yourself.
See the documentation on data classes for more information.
Related
I have a CounterScreenUiState data class with a single property called counterVal (integer). If I am updating the value of my counter from viewModel which of the following is the correct approach?
Approach A:
data class CounterUiState(
val counterVal: Int = 0,
)
class CounterViewModel : ViewModel() {
var uiState by mutableStateOf(CounterUiState())
private set
fun inc() {
uiState = uiState.copy(counterVal = uiState.counterVal + 1)
}
fun dec() {
uiState = uiState.copy(counterVal = uiState.counterVal - 1)
}
}
or
Approach B:
data class CounterUiState(
var counterVal: MutableState<Int> = mutableStateOf(0)
)
class CounterViewModel : ViewModel() {
var uiState by mutableStateOf(CounterUiState())
private set
fun inc() {
uiState.counterVal.value = uiState.counterVal.value + 1
}
fun dec() {
uiState.counterVal.value = uiState.counterVal.value - 1
}
}
For the record, I tried both approach and both works well without unnecessary re-compositions.
Thanks in Advance!!!
So to summarize, "implementation" and "performance" wise, your'e only
choice is A.
This is not true. It's a common pattern that is used other Google's sample apps, JetSnack for instance, and default functions like rememberScrollable or Animatable are the ones that come to my mind. And in that article it's also shared as
#Stable
class MyStateHolder {
var isLoading by mutableStateOf(false)
}
or
#Stable
class ScrollState(initial: Int) : ScrollableState {
/**
* current scroll position value in pixels
*/
var value: Int by mutableStateOf(initial, structuralEqualityPolicy())
private set
// rest of the code
}
Animatable class
class Animatable<T, V : AnimationVector>(
initialValue: T,
val typeConverter: TwoWayConverter<T, V>,
private val visibilityThreshold: T? = null,
val label: String = "Animatable"
) {
internal val internalState = AnimationState(
typeConverter = typeConverter,
initialValue = initialValue
)
/**
* Current value of the animation.
*/
val value: T
get() = internalState.value
/**
* The target of the current animation. If the animation finishes un-interrupted, it will
* reach this target value.
*/
var targetValue: T by mutableStateOf(initialValue)
private set
}
Omitted some code from Animatable for simplicity but as can be seen it's a common pattern to use a class that hold one or multiple MutableStates. Even type AnimationState hold its own MutableState.
You can create state holder classes and since these are not e not variables but states without them changing you won't have recompositions unless these states change. The thing needs to be changed with option B is instead of using
data class CounterUiState(
var counterVal: MutableState<Int> = mutableStateOf(0)
)
You should change it to
class CounterUiState(
var counterVal by mutableStateOf(0)
)
since you don't need to set new instance of State itself but only the value.
And since you already wrap your states inside your uiState there is no need to use
var uiState by mutableStateOf(CounterUiState())
private set
you can have this inside your ViewModel as
val uiState = CounterUiState()
or inside your Composable after wrapping with remember
#Composable
fun rememberCounterUiState(): CounterUiState = remember {
CounterUiState()
}
With this pattern you can store States in one class and hold variables that should not trigger recomposition as part of internal calculations and it's up to developer expose these non-state variables based on the design.
https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt
#Stable
class SearchState(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
categories: List<SearchCategoryCollection>,
suggestions: List<SearchSuggestionGroup>,
filters: List<Filter>,
searchResults: List<Snack>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var categories by mutableStateOf(categories)
var suggestions by mutableStateOf(suggestions)
var filters by mutableStateOf(filters)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.Categories
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
}
Also for skippibility
Compose will treat your CounterUiState as unstable and down the road
it will definitely cause you headaches because what ever you do,
This is misleading. Most of the time optimizing for skippability is premature optimization as mentioned in that article and the one shared by originally Chris Banes.
Should every Composable be skippable? No.
Chasing complete skippability for every composable in your app is a
premature optimization. Being skippable actually adds a small overhead
of its own which may not be worth it, you can even annotate your
composable to be non-restartable in cases where you determine that
being restartable is more overhead than itโs worth. There are many
other situations where being skippable wonโt have any real benefit and
will just lead to hard to maintain code. For example:
A composable that is not recomposed often, or at all.
I have this Firestore document Quiz_android that looks list this:
It is a simple array with maps in it. Now I would like to bind those results to some objects in Kotlin. Therefore I have made the following:
data class QuizBody(
val questions: List<Question>
)
data class Question(
val question: String,
val answers: List<String>,
val answer: Int
)
A Quizbody is just all the questions for the quiz in a list, and in that list, I have classes of Question which should be able to store all the data from the call.
But how do I bind the result from the call to those objects?
suspend fun getQuestions(quizToGet: String) {
try {
//firestore has support for coroutines via the extra dependency we've added :)
withTimeout(5_000) {
firestore.collection("Quizzes").document(quizToGet).get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val result = task.result
if (result.exists()) {
val myObject = result.toObject(QuizBody::class.java)
println(myObject)
}
}
}
}
} catch (e: Exception) {
throw QuizRetrievalError("Retrieving a specific quiz was unsuccessful")
}
}
I have made this but this does not work.
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.hva.madlevel7task2, PID: 3995
java.lang.RuntimeException: Could not deserialize object. Class com.hva.madlevel7task2.model.QuizBody does not define a no-argument constructor. If you are using ProGuard, make sure these constructors are not stripped
Edit:
I have updated the data class:
data class QuizBody(
var questions: List<Question>? = null
)
data class Question(
var question: String? = null,
var answers: List<String>? = null,
var answer: Int? = null
)
suspend fun getQuestions(quizToGet: String) it still the same, now I get this in the console:
I/QuizViewModel: function: getListQuestions
W/Firestore: (24.1.1) [CustomClassMapper]: No setter/field for Questions found on class com.hva.madlevel7task2.model.QuizBody (fields/setters are case sensitive!)
I/System.out: QuizBody(questions=null)
The following error:
java.lang.RuntimeException: Could not deserialize object. Class com.hva.madlevel7task2.model.QuizBody does not define a no-argument constructor.
Is very straightforward in my opinion. Your class "QuizBody" does not have a no-argument constructor. When you try to deserialize an object that comes from a Firestore database, the Android SDKs require that the class should mandatorily have a default no-argument constructor.
In Kotlin, the data classes don't provide a default no-arg constructor if all the properties of the class are declared with val. For such properties, Kotlin requires that their values be specified in the constructor since they can't possibly change later. So this is required because we need to ensure the compiler that all the properties have an initial value. You can provide to all of the properties an initial value of null or any other value you find more appropriate. So your classes should look like this:
data class QuizBody(
var questions: List<Question>? = null
๐ ๐
)
data class Question(
var question: String? = null,
var answers: List<String>? = null,
var answer: Int? = null
)
Now adding the properties in the constructor, Kotlin will automatically generate a default no-argument constructor. In this way, the Firebase Android SDK will be able to use to generate setters for each property. If don't make this change, you won't be able to use automatic deserialization. You'll have to read the value for each property out of the DocumentSnapshot object and pass them all to Kotlin's constructor.
Edit:
if (task.isSuccessful) {
val document = task.result
if (document.exists()) {
val myObject = document.toObject(QuizBody::class.java)
println(myObject)
}
}
I have the below working code which uses a dropdown to update the satusFilterFlow to allow for the filtering of characters through the getCharacterList call. The getCharacterList call uses the jetpack paging and returns Flow<PagerData<Character>>.
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
// private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.flatMapLatest{ statusFilter ->
characterRepository.getCharacterList(null, statusFilter.status)
.cachedIn(viewModelScope)
.flowOn(Dispatchers.IO)
}.asLiveData()
Given the above working solution, what is the correct flow extension to allow for me to add multiple StateFlows as I build out additional filters (e.g. SearchFilter).
I have tried combineTransorm as follows:
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.combineTransform(searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
However, this gives me a "Not enough information to infer type variable R" error.
The usual way to understand and/or fix those errors is to specify types explicitly in the function call:
statusFilterFlow.combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(searchFilterFlow) { ... }
This is orthogonal to the problem at hand, but I'd also suggest using the top-level combineTransform overload that takes all source flows as argument (instead of having the first one as receiver), so there is a better symmetry. Since I believe there is no reason one of the filters is more special than the other.
All in all, this gives:
val listData: LiveData<PagingData<Character>> =
combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(statusFilterFlow, searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
For anymore else, this is too complex or doesn't work out for you ... Use Combine then flatMap latest on the top of that.
private val _selectionLocation: MutableStateFlow<Location?> = MutableStateFlow(null)
val searchKeyword: MutableStateFlow<String> = MutableStateFlow("")
val unassignedJobs: LiveData<List<Job>> =
combine(_selectionLocation, searchKeyword) { location: Location?, keyword: String ->
Log.e("HomeViewModel", "$location -- $keyword")
location to keyword
}.flatMapLatest { pair ->
_repo.getJob(Status.UNASSIGNED, pair.first).map {
Log.e("HomeViewModel", "size ${it.size}")
it.filter { it.desc.contains(pair.second) }
}
}.flowOn(Dispatchers.IO).asLiveData(Dispatchers.Main)
I have an array of feedback channels because (outside of question scope) in my ViewModel.
Now, I don't want to expose my MutableLiveData to outside my Viewmodel.
So, i make a private list of LiveData objects, but compiler complains of "Useless Cast"
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels
get() = _feedbackChannels.map{
#Suppress("USELESS_CAST") // it is not useless as it no longer exposes the mutableLiveData
it as LiveData<*>
}
Why do I get USELESS_CAST warning?
Compiler doesn't realize you're doing it only to force implication of property type.
Just specify type explicitly and you'll be able to drop the cast entirely. You won't even have to use map, a simple toList() will do:
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels : List<LiveData<FeedbackEvent>>
get() = _feedbackChannels.toList()
Clearly the compiler doesn't understand the point of the cast. In order to do this in a more explicit way and remove the costly map function, you can just upcast it like this:
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels: Array<out LiveData<FeedbackEvent>>
get() = _feedbackChannels
Edit
If you wanted to expose a List specifically (avoid exposing a mutable array) then you should probably just create one in the first place:
private val _feedbackChannels = List(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels: List<out LiveData<FeedbackEvent>>
get() = _feedbackChannels
I'm currently trying the new Jetpack ViewModel with savedState.
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-rc01'
I'm using 1 Activity and trying to share 1 ViewModel with 2 Fragments but when I try to start the second fragment I get the error from the title.
This is how I'm calling the ViewModel with the savedInstance:
val repository = (activity?.application as App).getRepository()
viewModel = activity?.run {
ViewModelFactory(repository, this, savedInstanceState).create(MainViewModel::class.java)
} ?: throw Exception("Invalid Activity")
And this is my log:
java.lang.IllegalArgumentException: SavedStateProvider with the given key is already registered
at androidx.savedstate.SavedStateRegistry.registerSavedStateProvider(SavedStateRegistry.java:111)
at androidx.lifecycle.SavedStateHandleController.attachToLifecycle(SavedStateHandleController.java:50)
at androidx.lifecycle.SavedStateHandleController.create(SavedStateHandleController.java:70)
at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:67)
at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:84)
at com.xxx.yyy.presentation.details.DetailsFragment.onCreate(DetailsFragment.kt:29)
at androidx.fragment.app.Fragment.performCreate(Fragment.java:2586)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:838)
at androidx.fragment.app.FragmentTransition.addToFirstInLastOut(FragmentTransition.java:1197)
at androidx.fragment.app.FragmentTransition.calculateFragments(FragmentTransition.java:1080)
at androidx.fragment.app.FragmentTransition.startTransitions(FragmentTransition.java:119)
at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1866)
at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1824)
at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1727)
at androidx.fragment.app.FragmentManagerImpl$2.run(FragmentManagerImpl.java:150)
Looks like it's trying to use a SavedState which was already registered and hence the error? I thought that was the whole point of the library. Can anyone help or point on how to use this passing arguments to the ViewModel and using the savedStateHandle?
You should never be calling create yourself - by doing so, you're not actually using the retained ViewModel that is already created, causing the AbstractSavedStateViewModelFactory to attempt to register the same key more than once.
Instead, you should be passing your ViewModelFactory to a ViewModelProvider instance to retrieve the already existing ViewModel or creating it only if necessary:
val repository = (activity?.application as App).getRepository()
viewModel = activity?.run {
val factory = ViewModelFactory(repository, this, savedInstanceState)
ViewModelProvider(this, factory).get(MainViewModel::class.java)
} ?: throw Exception("Invalid Activity")
If you depend on fragment-ktx of version 1.1.0 or higher, you can instead use the by activityViewModels Kotlin property delegate, which lazily uses ViewModelProvider under the hood:
val viewModel: MainViewModel by activityViewModels {
val repository = (activity?.application as App).getRepository()
ViewModelFactory(repository, requireActivity(), savedInstanceState)
}
When I ran into the given key is already registered error, I did go through this, this and this. But I haven't found anything helpful. In my case, the issue was with Moshi Json Generators.
File1: VictimViewModel.kt
class VictimViewModel constructor(
private val searchManager: SearchManager,
) : ViewModel() {
}
File 2: SearchManager.kt
class SearchManager internal constructor(
private val SearchApi: SearchApi
) {
fun searchString(token: String): Either<A, B> {
return searchApi
.searchString(token)
.process(
this::parseResponse
)
}
}
File 3: SearchAPI.kt
internal interface SearchApi {
#GET("$BASE_PATH/search_string")
fun searchString(#Query("search_string") token: String): NetworkEither<SearchResponse>
}
File 4: SearchResponse.kt (This file has the root cause)
data class SearchResponse(
#Json(name = "suggestions")
val suggestions: List<String>
)
What is wrong with the above code?
I forgot to add #JsonClass(generateAdapter = true) annotate to SearchResponse data class. Which has broken the injection mechanism and started throwing key is already registered error while initiating the ViewModel. Error messaging and the actual issue are totally irrelevant. So, it took a while for me to understand the problem.
Solution:
Update the File 4: SearchResponse.kt as below
#JsonClass(generateAdapter = true)
data class SearchResponse(
#Json(name = "suggestions")
val suggestions: List<String>
)