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.
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.
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.
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 want to create custom chainig to prevent some repetition.
I am a bit unclear on how to define it
I have a lot of :
someLiveData.observe(this) { objectWithTextAndVisibility->
textView.text = objectWithTextAndVisibility.text
textView.visibility = objectWithTextAndVisibility.visibility
}
i want to write something that will look as follows
someLiveData.observe(this).bind(textView).on(text)
and it will do the same thing
is there a way to define this ?
If I understood your question correctly, a generic structure like this may achieve what you want to do:
infix fun <T> LiveData<T>.observe(owner: LifecycleOwner) = LiveDataHolder(this, owner)
class LiveDataHolder<T>(val liveData: LiveData<T>, val owner: LifecycleOwner)
infix fun <T, S> LiveDataHolder<T>.bind(subject: S) = LiveDataBinder(this, subject)
class LiveDataBinder<T, S>(val liveDataHolder: LiveDataHolder<T>, val subject: S)
infix fun <T : VisibilityCarrier> LiveDataBinder<T, TextView>.on(textSelector: (T) -> String) {
liveDataHolder.liveData.observe(liveDataHolder.owner) {
subject.text = textSelector(it)
subject.visibility = it.visibility
}
}
interface VisibilityCarrier {
val visibility: Int
}
This should give you good flexibility and prevent a lot of repetition, however at the cost of adding some obscurity to your code base, and not conveying what it does right alway.
Additional and different behaviors can be added by implementing new LiveDataBinder extension functions, like the on function, even for different kinds of objects and expected interfaces (instead of only this combination of TextView and VisibilityCarrier, even by maintaining the on name) and so on.
The use style can vary a lot with this, being like the ones below (considering Data implements VisibilityCarrier interface and provides a text String property):
liveData.observe(this).bind(textView).on { it.text }
liveData.observe(this) bind textView on { it.text }
liveData.observe(this).bind(textView).on(Data::text)
liveData.observe(this) bind textView on Data::text
This on implementation also allows for defining which property to use as text and different classes as well, like so:
anotherLiveData.observe(this).bind(textView).on(AnotherData::someText)
anotherLiveData.observe(this).bind(textView).on(AnotherData::anotherText)
UPDATE: After reading the explanation in the comments I think I got it, and believe this should address the point (where Data contains text and visibility properties):
infix fun <T : Data, S : View> LiveDataBinder<T, out S>.on(textProperty: KMutableProperty1<S, in String>) {
liveDataHolder.liveData.observe(liveDataHolder.owner) {
textProperty.set(subject, it.text)
subject.visibility = it.visibility
}
}
In the case of TextView and other Java defined classes, compiler will complain about synthetic access, and this problem could be addressed like in the following snippet (Kotlin views should be fine without this kind of workaround):
var TextView.text_: CharSequence
get() = text
set(value) { text = value }
And the usage would be like:
liveData.observe(this).bind(textView).on(TextView::text_)
liveData.observe(this).bind(customView).on(CustomView::someText)
UPDATE 2: A better approach as suggested by Tenfour04 (thanks). The following will avoid the synthetic property access compiler error:
infix fun <T : Data, S : View> LiveDataBinder<T, S>.on(textProperty: S.(String) -> Unit) {
liveDataHolder.liveData.observe(liveDataHolder.owner) {
subject.textProperty(it.text)
subject.visibility = it.visibility
}
}
And the usage would be like (without needing the text_ property extension):
liveData.observe(owner).bind(textView).on(TextView::setText)
After reading your comments on the other answer, I think I see what you're trying to do.
If I'm correct, you want to use the builder pattern to first bind something that is set (a setter), and then specify a getter/mapper of the data type to get some sub-data type that is applied with that setter.
So you can set up a couple of intermediate classes to do it like this:
fun <T> LiveData<T>.observe(owner: LifecycleOwner) = BindableObserver<T>().also { observe(owner, it) }
class BindableObserver<D>: Observer<D> {
private var boundSetter: BoundSetter<D, *>? = null
fun <S> bind(setter: (S)->Unit) = BoundSetter<D, S>(setter).also { boundSetter = it }
override fun onChanged(t: D) {
boundSetter?.execute(t)
}
}
class BoundSetter<D, S>(private val setter: (S)->Unit) {
private var dataGetter: ((D)->S)? = null
fun on(getter: (D)->S) {
dataGetter = getter
}
fun execute(newValue: D) {
val subData = dataGetter?.invoke(newValue) ?: return
setter.invoke(subData)
}
}
You can't simply pass a TextView to bind, because Kotlin won't know which property of TextView to set, so you pass the property using property syntax (::). Unfortunately, TextView has a bunch of setText() overloads, so you have to specify the input type as well.
Usage syntax would be like this:
someLiveData.observe(this)
.bind<String>(textView::setText)
.on(ObjectWithTextAndVisibility::text)
To avoid the need for specifying which function of a TextView to bind, you could add a helper function:
fun <D> BindableObserver<D>.bind(textView: TextView) = bind<String>(textView::setText)
and then usage would be closer to what you suggested:
someLiveData.observe(this)
.bind(textView)
.on(ObjectWithTextAndVisibility::text)
You could also use lambda syntax:
someLiveData.observe(this)
.bind(textView)
.on { it.text }
My goal: I have a simple class with a public
val reds = IntArray(10)
val greens = IntArray(10)
val blues = IntArray(10)
val lums = IntArray(10)
If someone modifies any red value, I'd like to update the lum value.
myObj.reds[5] = 100 // Should update myObj.lums[5] = reds[5]+greens[5]+blues[5]
The problems is that the by Delegates.observable seem to only be used for var objects - nothing mentions "and if you modify an element of an array, here is what gets triggered"
Maybe this isn't possible and I have to do all modifications through getters and setters - but I'd much rather have something trigger like an observable!
You will have to use a custom class instead, IntArray is mapped to primitive int[] array so it doesn't provide any place to inject callback - changing value like your example (myObj.reds[5] = 100) you only know when array is returned, but have no control over changes after that.
For example you can create class like this:
class IntArrayWrapper(size: Int,
val setterObs : ((index: Int, value: Int) -> Unit)? = null){
val field = IntArray(size)
val size
get() = field.size
operator fun iterator() = field.iterator()
operator fun get(i: Int) : Int {
return field[i]
}
operator fun set(i: Int, value: Int){
field[i] = value
setterObs?.invoke(i, value)
}
}
Operator functions will let you get values from underlying array with same syntax as if you were accessing it directly. setterObs argument in constructor lets you pass the "observer" for setter method:
val reds = IntArrayWrapper(10){index, value ->
println("$index changed to $value")
invalidateLums(index) // method that modifies lums or whatever you need
}
val a = reds[2] // getter usage
reds[3] = 5 // setter usage that triggers the setter observer callback
reds.field[4] = 3 // set value in backing array directly, allows modification without setter callback
Note that this imposes limitations, as you won't be able to freely use IntArray extension methods without referencing backing field nor will you be able to pass this class as an Array argument.
I do not know if it is the cleanest way of solving your problem but, you could use the ObservableList (FX observable collections):
var numbers: ObservableList<Int> = FXCollections.observableArrayList()
numbers.addListener(ListChangeListener<Int> {
//Do things on change
})
But as I mentioned, by adding these collections you are mixing FX components into your application, which I do not know if it is wanted or even if it works on various platforms like android!