today I'm trying about Jetpack compose and the ViewModel class.
Everything seems to be logic but I got some problems understanding the encapsulation.
// ViewModel
class MyViewModel: ViewModel() {
var uiStateList = mutableStateListOf(1,2,3)
privat set
private var _uiStateList2 = mutableStateListOf(11,22,33)
val uiState2 List<Int>
get() = _uiStateList2
fun addToUiStateList2() {
_uiStateList2.add(_uiStateList2.last()+11)
}
}
The first var is like in the Compose tutorials.
The construction of the second var looks like using MutableLiveData.
// Activity
#Composable
fun MyComposable(
modifier: Modifier = Modifier,
myViewModel: MyViewModel= viewModel()
) {
val uiStateList = myViewModel.uiStateList
val uiStateList2 = myViewModel.uiStateList2
Column {
uiStateList.forEach {
Text(it.toString())
}
Button(
onclick = {uiStateList.add(uiStateList.last()+1)},
content = {text(+1)}
)
uiStateList2.forEach {
Text(it.toString())
}
Button(
onclick = {
// uiStateList2.add(uiStateList2.last()+11) // not possible because of type List<Int>
myViewModel.addToUiStateList2() // possible, changing indirectly the value of uiStateList2
},
content = {text(+11)}
)
}
}
In the first case it's possible to modify the mutableStateList inside of the ViewModel by running just a simple function in the composable. Consequently it is possible to change the value from outside of the class directly.
In the second case I got no chance to change the data in the viewmodel. The var uiStateList2 is a imutable list which reflects the data from the private val from the viewmodel. If the function addToUiStateList2() is triggered, the original list changes and the composable will be recompositioned and everything is fine.
Why can I change the data of the var uiStateList inside the Composable, although the setter is set to private inside the ViewModel class?
In the documentation I read, that private setters could just be used inside the owning class. Do I think too complicated, or is this the usually aproach how everything is build?
Thanks for help Guys!
Private setter doesn't allow to set the value i.e myViewModel.uiStateList= //something. But you can still modify the list because it is mutable. If you want to restrict changing the state from outside viewModel second approach is preferred.
Related
New to Kotlin and to OOP in general
I have a TextView in my MainActivity which is linked to a var
var int = 0
findViewById<TextView>(R.id.textView).setText("$var")
The thing is I want to modify this var inside and adapter.
class Adapter(
var myContext: Context,
var resource: Int,
var values: ArrayList<List>
) : ArrayAdapter<List>(myContext, resource, values) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val element = values[position]
val view = LayoutInflater.from(myContext).inflate(resource, parent, false)
findViewByID<Button>(R.id.button).setOnClickListener{
int ++
View(myContext).findViewByID<TextView>(R.id.textView).setText("$var")
//here I get an error
}
}
}
I don’t know if this is clear enough. I saw I can use an interface Class but the thing is I don’t want to pass a data but to update one that already exists.
You are using ArrayAdapter. If you are going to use adapters, you should use RecyvlerView.Adapter. Since you are learning, this is a perfect point to scratch that and learn Compose instead, with that out of the way.
The interface solution is the correct solution:
interface MyAdapterCallback {
fun onButtonClicked(clickCount: Int)
}
Then implement it in your activity
class MyActivity : ..., MyAdapterCallback {
override fun onButtonClicked(clickCount: Int) {
findViewById<TextView>(R.id.textView).setText("$clickCount")
}
}
You have to pass it to your adapter, this should be the same for array adapter or recycler, because is passing it in the constructor and then using it.
class Adapter(
...,
private val callback: MyAdapterCallback
) {
...setOnClickListener{
int ++ //this will work as long as int here is a valid thing
callback.onButtonClicked(int)
}
}
You might see this pattern with the delegate naming instead, I'm using callback just to try to make it clearer.
Another dirty solution would be to, pass the view to the adapter.
class Adapter(
...,
private val myView: TextView
) {
...setOnClickListener{
int ++ //this will work as long as int here is a valid thing
myView.text = "$int"
}
}
That is a very bad solution because it breaks the separations of concern principle, you should use it only for debugging.
Finally, the problem that you are currently having is this:
View(myContext).findViewByID<TextView>(R.id.textView).setText("$var")
That is instantiating a new View and inside that View you are trying to find the R.id.textView, that view is completely new so it has nothing inside. Your R.id.textView is in the activity layout, a completely different view. So the method findViewByID returns null. However you declare that the method should found non null TextView that why it crashes, if you change it to TextView? then it will be handle as nullable and it won't crash, but is pointless because it doesn't exist. The method findViewByID doesn't search in every place, just inside the View you are accessing.
I hope to remember a string which is from stringArrayResource in Code A , but I get the the Error A. How can I fix it?
And more, I find more variables can't be wrapped with remember, such as val context = LocalContext.current , why?
Error A
Composable calls are not allowed inside the calculation parameter of inline fun remember(calculation: () -> TypeVariable(T)): TypeVariable(T)
Code A
#Composable
fun DialogForDBWarningValue(
preferenceState:PreferenceState
) {
val context = LocalContext.current //I can't wrap with remember
val itemName =remember{ stringArrayResource(R.array.dBWarning_Name) } //I can't wrap with remember
}
#Composable
inline fun <T> remember(calculation: #DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
The reason for that error is #DisallowComposableCalls annotation
This will prevent composable calls from happening inside of the
function that it applies to. This is usually applied to lambda
parameters of inline composable functions that ought to be inlined but
cannot safely have composable calls in them.
I don't know if accessing resources and getting strings would have any impact on performance but as an alternative this can be done using nullable properties, i don't think it's good practice to have nullable objects while you don't have to, by only getting resources once your String is null or an object that holds Strings and sets them on Composition or configuration changes if you wish to change new ones.
class StringHolder() {
var str: String = ""
}
val stringHolder = remember(LocalConfiguration.current) {
StringHolder()
}.apply {
this.str = getString(R.string.dBWarning_Name)
}
I have the following ViewModel
#HiltViewModel
class ShareViewModel #Inject constructor(
private val taskRepository: TaskRepository
): ViewModel() {
private val searchAppBarStateMutableState: MutableState<SearchAppBarState> = mutableStateOf(SearchAppBarState.CLOSED)
val searchAppBarState: State<SearchAppBarState> = searchAppBarStateMutableState
private val listOfTaskMutableStateFlow = MutableStateFlow<List<TodoTaskEntity>>(emptyList())
val listOfTaskStateFlow = listOfTaskMutableStateFlow.asStateFlow()
}
I never expose mutableStateFlow as in the example above. And SonarLint will show a warning when doing this.
MutableStateFlow" and "MutableSharedFlow" should not be exposed
So I apply the same technique to the mutableState
However, If I do like this below, I don't get any warning.
val searchAppBarStateMutableState: MutableState<SearchAppBarState> = mutableStateOf(SearchAppBarState.CLOSED)
Just wondering what is the best practice for using MutableState with jetpack compose.
To use mutableState with viewmodel, define mutableState with private setter inside viewmodel, ex -
var isVisible by mutableState(false)
private set
By doing above we can read the mutable state from outside of viewmodel but not update it. To update create a public function inside viewmodel, ex -
fun setVisibility(value: Boolean) {
isVisible = value
}
By making a setter function we are following separation of concerns and having a single source of truth for editing the mutableState.
I think the error is in that you are setting
val searchAppBarState: State<SearchAppBarState> = searchAppBarStateMutableState
if you want to share private value as not mutable you shouldn't set it equal rather you can use get modifier
val searchAppBarState: State<SearchAppBarState> get() = searchAppBarStateMutableState
Also it would be better to name it with underscore as many developers are used to like:
private val _searchAppBarState: MutableState<SearchAppBarState> = mutableStateOf(SearchAppBarState.CLOSED)
val searchAppBarState: State<SearchAppBarState> get() = _searchAppBarState
I'm experimenting with Jetpack Compose and am trying to make a Canvas with a number of rectangles, each of which is filled or not depending on the value of a Bool at a corresponding index of an array. When an element of that array changes, the Canvas should redraw.
I've discovered I can't simply make a LiveData array of Booleans, or a list, since for that to work the entire object needs to be recreated each time for setValue to trigger and be observed. So I've made an array of LiveData booleans in a view model;
class StripeModel : ViewModel() {
private val _values = Array<MutableLiveData<Boolean>>(50) { MutableLiveData(false) }
val values = Array<LiveData<Boolean>>(50) {i->_values[i]}
fun onValueChanged(index: Int, newVal: Boolean)
{
_values[index].value = newVal;
}
}
If I pass that view model to my test function, I can look at a particular member of it in a way that causes the canvas to recompose on change using something like
val state by model.values[5].observeAsState();
This would be fine if I had a different canvas for each element, but I don't. So I want my single canvas to be looking at all of them, and refresh if any change. The sensible way to do this without explicitly declaring a state variable for each member seemed to be to create an array of states, and the way I came up with to do that was
val states = Array<State<Boolean?>>(20){ i->model.values[i].observeAsState()}
However, this flags an error because observeAsState needs to be in a function marked #Composable. The outer function itself is, but it seems that's not inherited by the lambda. And if I try and mark the lambda as #Composable then it makes Android Studio very unhappy and tells me to report it as a bug. Doesn't crash the environment but I can't compile it.
The reason I have a strong desire to do this in a single canvas is because I want to be able to click a single item to change its value, or drag across a number of items to change a number of them all at once. That seems like it should be a lot more efficient by handling all the coordinates within one widget rather than having 50 separate widgets and trying to figure out which is at the present location during the drag.
So, how can I make my composable function observe n array elements without explicitly writing n lines that create n variables?
Following a few days away I've worked through some of the suggestions people have given.
#cactustictacs suggested the simple approach of making the LiveData array of Booleans. I hadn't actually tried this. Something I'd read made me think it wouldn't work so I tried going more complicated. However, I can't get it to work.
I've simplified the code so it's postable, minus imports.
class StripeModel : ViewModel() {
private val _values = MutableLiveData<Array<Boolean>>(Array<Boolean>(20) {false});
val values: LiveData<Array<Boolean>> = _values;
fun onValueChanged(index: Int, newVal: Boolean)
{
_values.value?.set(index, newVal);
_values.value=_values.value;
}
}
class MainActivity : ComponentActivity() {
private val stripeModel by viewModels<StripeModel>();
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Surface{
MaterialTheme{
TestCanvas(stripeModel);
}
}
}
}
}
#Composable
fun TestCanvas(model : StripeModel)
{
val state by model.values.observeAsState();
Canvas(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onPress = { it -> model.onValueChanged(5, !state?.get(5)!!) }
)
}
.background(Color.LightGray),
onDraw={
val offset = Offset(100f,100f);
val size = Size(200f,200f);
if (state?.get(5) == true) {
drawRect(
brush = SolidColor(Color.Blue),
size = size,
topLeft = offset
)
}
drawRect(
brush = SolidColor(Color.White),
size = size,
topLeft = offset,
style = Stroke(width = 10f)
)
}
)
}
So there's an arbitrary 20 elements of which I'm just looking at index 5. By changing the initialiser I can see that the value is being read on draw. In the debugger I can see that a tap on the screen fires onValueChanged which changes the stored value. However that doesn't cause TestCanvas to recompose.
#chuckj suggested using a mutableStateListOf<MutableState>. If I change my view model to
class StripeModel : ViewModel() {
val values = mutableStateListOf<MutableState<Boolean>>()
init {
for (i in 0..20)
{
var s = mutableStateOf<Boolean>(false);
values.add(s);
}
}
fun onValueChanged(index: Int, newVal: Boolean)
{
values[index] = mutableStateOf<Boolean>(newVal);
}
}
and I look at it using
val state = model.values;
the behaviour is the same- no display update on tap.
#Robert Nagy suggested a LiveData<List>. So I created the ViewModel as
class StripeModel : ViewModel() {
private val _values : MutableList<Boolean> = Array<Boolean>(20) {false}.toMutableList();
val values = mutableStateOf(_values);
fun onValueChanged(index: Int, newVal: Boolean)
{
_values.set(index,newVal);
values = values;
}
}
and look at it using
val state by model.values;
Here, it won't build if I include the line values = values. Otherwise, though it builds and runs, it still doesn't cause a recompose.
I've not pasted the whole of the code each time, but it's my understanding that by setting that 'state' value at the start of the composable, any change will trigger a re-run of that function from the start, so only that line is relevant?
So, thanks to those who've commented. Is there something I'm doing wrong that this edit's made apparent?
LiveData<List<T>> Should definitely work.
An example, Screens with lists are commonly modified with:
fun removeLastItem(){
_items.value = _items.value.dropLast(1)
}
I suspect something is off at the subscriber/observer.
Im trying to get some data out of other ViewModels inside another ViewModel to make my code smaller, but im having a problem trying to implement what already worked on a fragment or in a activity, this is what i got:
class ObraConMediaViewModel(private val context: ViewModelStoreOwner,
private val id: Int): ViewModel(), LifecycleObserver {
var allObras: LiveData<ArrayList<ObraConMedia>>
private lateinit var viewModelobras: ViewModelObras
private lateinit var viewModelMediaObra: ViewModelMediaObra
val repositoryobras =ObrasRepository()
val repositoryMediaObra = MediaObraRepository()
val viewModelFactoryobras = ViewModelFactoryObras(repositoryobras)
val viewModelMediaObraFactory = ViewModelMedIaObraFactory(repositoryMediaObra)
init{
viewModelobras = ViewModelProvider(context, viewModelFactoryobras)
.get(ViewModelObras::class.java) // requireActivity() when called
viewModelMediaObra = ViewModelProvider(context, viewModelMediaObraFactory)
.get(ViewModelMediaObra::class.java)
viewModelobras.getObras(id)
viewModelobras.myResponse.observe(this , Observer { response ->
if (response.isSuccessful){
Log.d("Response", response.body()?.ans?.get(0)?.autor)
Log.d("Response", response.body()?.ans?.get(1)?.autor)
}else{
Log.d("Response", response.errorBody().toString())
}})
viewModelMediaObra.getMediaObra(Constantes.PRUEBA_ID)
viewModelMediaObra.myResponse.observe(this, Observer { response ->
if (response.isSuccessful){
Log.d("Response", response.body()?.ans?.get(0)?.filePath)
}
})
}}
I was having trouble with the Observer but extending the class to LifecycleObserver fixed it, i have no idea if this will even work but the only error that i have right now its the owner of the .observe(this,....), i dont seem to find a way to pass a lifecycleowner from the fragment to this viewmodel. All the variables i need to make this viewmodel work are inside those two responses. If this is a very bad way to do it please tell me. Thanks for reading.
Kindly note that above approach is not correct.
One should not create a instance of ViewModel inside another ViewModel.
There is a possibility that one ViewModel may get destroyed before another. This will lead to garbage reference and memory leaks.
I would recommend you to create the instance of both View Models in an Activity/Fragment and then call respective methods of ViewModel from Activity/Fragment.
Also, as you want to make your code smaller and concise, I highly recommend you Shared ViewModel.
This Shared ViewModel can be used by two fragments.
Please refer to this link