Update value in an activity from an adapter (Kotlin) - kotlin

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.

Related

Jetpack Compose correct accessing of data from ViewModel

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.

Observe and respond to changes in elements of an array

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.

Trying to use ViewModels inside of another ViewModel, Errors with LifecycleObserver and Ownership (Kotlin)

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

How to inject dependency using koin in top level function

I have top-level function like
fun sendNotification(context:Context, data:Data) {
...//a lot of code here
}
That function creates notifications, sometimes notification can contain image, so I have to download it. I`m using Glide which is wrapped over interface ImageManager, so I have to inject it. I use Koin for DI and the problem is that I cannot write
val imageManager: ImageManager by inject()
somewhere in my code, because there is no something that implements KoinComponent interface.
The most obvious solution is to pass already injected somewhere else imageManager as parameter of function but I dont want to do it, because in most cases I dont need imageManager: it depends on type of Data parameter.
Easiest way is to create KoinComponent object as wrapper and then to get variable from it:
val imageManager = object:KoinComponent {val im: ImageManager by inject()}.im
Btw its better to wrap it by some function, for example I use
inline fun <reified T> getKoinInstance(): T {
return object : KoinComponent {
val value: T by inject()
}.value
}
So if I need instance I just write
val imageManager:ImageManager = getKoinInstance()
or
val imageManager = getKoinInstance<ImageManager>()
I did it in this way
fun Route.general() {
val repo: OperationRepo by lazy { GlobalContext.get().koin.get() }
...
}

How to reference "this" within anonymous listeners when using short notation?

In Kotlin, is there a way to reference the listener instance when using this short notation for anonymous classes? In this case this refers to outer context (e.g. the Activity instance) where view is defined:
view.setOnClickListener {
val self: View.OnClickListener = this // Not compiling, "this" references outer context
}
When using the longer notation where you explicitly state the interface to be implemented and where you explicitly override the callback method, the listener can be referenced through this:
view.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View) {
val self: View.OnClickListener = this // Ok
}
})
You can get that resolved by adding an #ActivityName in front of 'this' reference
For example if your Activity name was MainActivity the solution would be:
view.setOnClickListener {
val self: View.OnClickListener = this#MainActivity
}
The term short notation for anonymous classes is not entirely correct. It's actually a short notation for anonymous functions, i.e. lambdas. Of course under the hood they are compiled to classes but from a programming language point of view, anonymous functions don't have identities and therefore it doesn't make sense to refer to their instances via this.
val animation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
val layoutParam: RelativeLayout.LayoutParams? = playerView.layoutParams as RelativeLayout.LayoutParams?
layoutParam?.topMargin = convertDpToPixel(position, this#SurahActivity).toInt()
playerView.layoutParams = layoutParam
}
}
Somewhat related - here is an example of removing a listener from the listener itself that came about after not being able to refer to itself as reported by the accepted answer
#Suppress("JoinDeclarationAndAssignment")
fun View.foo() {
// can't combine this with the assignment since it is referenced within the
// body of the layout listener to remove itself
lateinit var layoutListener: ViewTreeObserver.OnGlobalLayoutListener
layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
// ... do something ...
viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
}
viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
}
I don't typically see lateinit used in a local method field so this wasn't immediately obvious to me