Android Kotlin: Setting fragment TextView background on start from ViewModel - android

I am confused by a certain inconsistency in my code, where only part of the data is loading. I am trying to set up a grid of TextViews in my fragment, which read from a list variable called board on the ViewModel for that fragment. The TextView text is set as board[n].text from the view model, where n is its index in the list, and this loads just fine. I am also trying to set the TextView background to one of three background resources, which are saved as an int board[n].marking on the view model.
This does not work. It seems that it is trying to load the background for each TextView before board has been fully initialized in the view model, but it does not seem to try to do the same for the TextView text. Here are the relevant parts of my code. First, the XML layout:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".screens.game.GameFragment">
<data>
<import type="android.view.View"/>
<variable
name="gameViewModel"
type="com.example.mygametitle.screens.game.GameViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
(...)
<TextView
android:id="#+id/field13"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="#dimen/board_vertical_margin"
android:background="#{gameViewModel.board[2].marking}"
android:onClick="#{ () -> gameViewModel.openGameDialog(2, field13)}"
android:text="#{gameViewModel.board[2].text}"
(...)
There are 25 fields like that. All of the text loads properly, but none of the background images load. If instead I hardcode the background I want, as such, it loads properly:
android:background="#drawable/board_fieldbackground_checked" . This won't work for me though, as I need to read what each entry's background is upon startup--they don't all start checked.
On the view model, board is made by reading a set of 25 entries from a Room database, each including (among other info) a text string and a marking int. These all update properly--if I use a debug function to print out the contents of my board, they all have the proper text and marking upon closing and reopening the fragment. When the fragment opens, all the text is correct, but the backgrounds are not. Any ideas on why my backgrounds aren't loading the same way the text is?
Here's some of the relevant viewmodel code:
class GameViewModel(
val database: BoardDatabaseDao,
application: Application,
val boardTitle: String) : AndroidViewModel(application) {
val BG_UNMARKED = R.drawable.board_fieldbackground_bordered
val BG_CHECKED = R.drawable.board_fieldbackground_checked
val BG_MISSED = R.drawable.board_fieldbackground_missed
private val thisBoardEntries = MutableLiveData<List<BoardField>?>()
private val _board = MutableLiveData<List<BoardField>>()
val board: LiveData<List<BoardField>>
get() = _board
private suspend fun getEntries() : List<BoardField>? {
Log.i("GameViewModel", "Running database.getFromParent(boardTitle), function getEntries().")
val entries = database.getFromParent(boardTitle)
return entries
}
init {
viewModelScope.launch {
Log.i("GameViewModel", "Start viewModelScope.launch on init block.")
thisBoardEntries.value = getEntries()
if (thisBoardEntries.value?.isEmpty()!!) {
Log.i(
"GameViewModel",
"allEntries.value is EMPTY, seemingly: ${thisBoardEntries.value}, should be empty"
)
} else {
Log.i(
"GameViewModel",
"allEntries.value is NOT empty, seemingly: ${thisBoardEntries.value}, should be size 25"
)
_board.value = thisBoardEntries.value
}
}
}
fun markFieldMissed(index: Int, view: TextView) {
Log.i("GameViewModel", "My Textview looks like this: $view")
_board.value!![index].marking = BG_MISSED
view.setBackgroundResource(BG_MISSED)
Log.i("GameViewModel", "Set background to $BG_MISSED")
val color = getColor(getApplication(), R.color.white_text_color)
view.setTextColor(color)
viewModelScope.launch {
val markedField = getEntryAtIndex(boardTitle, convertIndexToLocation(index))
Log.i("GameViewModel", "I think markedField is $markedField")
if (markedField != null) {
markedField.marking = BG_MISSED
update(markedField)
Log.i("GameViewModel", "Updated field with $BG_MISSED marking on DB: $markedField")
}
}
}
fun markFieldChecked(index: Int, view: TextView) {
_board.value!![index].marking = BG_CHECKED
view.setBackgroundResource(BG_CHECKED)
Log.i("GameViewModel", "Set background to $BG_CHECKED")
val color = getColor(getApplication(), R.color.white_text_color)
view.setTextColor(color)
viewModelScope.launch {
val markedField = getEntryAtIndex(boardTitle, convertIndexToLocation(index))
Log.i("GameViewModel", "I think markedField is $markedField")
if (markedField != null) {
markedField.marking = BG_CHECKED
update(markedField)
Log.i("GameViewModel", "Updated field with $BG_CHECKED marking on DB: $markedField")
}
}
}
fun debugPrintEntries() {
Log.i("GameViewModel", "DebugPrintEntries function: ${_board.value}")
}
(2020-11-05) Edit 1: Part of the issue was indeed a resource not being read as such. I made the following additions/changes in my layout XML, which gets me a bit further:
<data>
<import type="androidx.core.content.ContextCompat"/>
(...)
</data>
<TextView
(...)
android:background="#{ContextCompat.getDrawable(context, gameViewModel.BG_CHECKED)}"
(...)
With a hardcoded resource for BG_CHECKED as my background image, everything loads and displays nicely. The problem is once again that the background is not read from board[4].marking (which contains BG_CHECKED as its value), although the text has no problem being read from board[4].text
The following replacement in the layout XML does not work, causing an exception: Caused by: android.content.res.Resources$NotFoundException: Resource ID #0x0 with the line
android:background="#{ContextCompat.getDrawable(context, gameViewModel.board[4].marking)}"

I haven't used data binding, but I think it might be because you're just providing an Int as the background, which happens to represent a resource ID - but the data binding doesn't know that, so it doesn't know it needs to resolve it to a drawable value in resources? When you set it manually you're explicitly telling it to do that by using the #drawable syntax
Here's a blog where someone runs into something similar (well that situation anyway, but with colours) - their second solution is to add a ContextCompat import to the data block, and then use that to do aContextCompat.getColor lookup in the data binding expression. Maybe you could do something similar to get the drawable you need

Related

How to Correctly Save the State of a Custom View in Android

At the very beginning, let's try to answer the question - why should we actually save the state of the views?
Imagine a situation in which you fill a large questionnaire in a certain application on your smartphone . At some point, you accidentally rotate the screen or you want to check something in another application and, after returning to the questionnaire, it turns out that all the fields are empty. Such a situation effectively discourage users from using the application. For the purposes of this example, let's create a view:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<androidx.appcompat.widget.SwitchCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
And its corresponding class, that inflates the xml:
class CustomSwitchViewNoId #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
init {
LayoutInflater.from(context).inflate(R.layout.view_custom_switch_no_id, this)
}
}
Instead of rotating the screen every time to check the effect, we can enable the appropriate option in the developer settings of the phone
(Settings -> Developer options -> Don’t keep activities -> turn it on).
As you can see - the state has not been saved. Let’s recall the hierarchy that is called by Android to save and read the state:
Save state:
saveHierarchyState(SparseArray container)
dispatchSaveInstanceState(SparseArray container)
onSaveInstanceState()
Restore state:
restoreHierarchyState(SparseArray container)
dispatchRestoreInstanceState(SparseArray container)
onRestoreInstanceState(Parcelable state)
Let’s see the internal implementation of saveHierarchyState:
public void saveHierarchyState(SparseArray container) {
dispatchSaveInstanceState(container);
}
Which leads us to dispatchSaveInstanceState:
#Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
super.dispatchSaveInstanceState(container);
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
View c = children[i];
if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
c.dispatchSaveInstanceState(container);
}
}
}
That calls for the container and every child dispatchSaveInstanceState(container):
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
Parcelable state = onSaveInstanceState();
if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
throw new IllegalStateException(
"Derived class did not call super.onSaveInstanceState()");
}
if (state != null) {
// Log.i("View", "Freezing #" + Integer.toHexString(mID)
// + ": " + state);
container.put(mID, state);
}
}
}
As we can see in the 3rd line, there is check if the view has an ID assigned to call onSaveInstanceState() and put the state into the container, where the ID is a key.
Let's add the needed ID and see what happens:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<androidx.appcompat.widget.SwitchCompat
android:id="#+id/customViewSwitchCompat"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<EditText
android:id="#+id/customViewEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
It works! Great, let’s add two more our custom views. In the end, we created this whole thing so as not to have to duplicate the code and check if everything is still working.
So the problems is the when including multiple view, their state is not correctly restored. And it looks as if all views retrieve the state of the last child. How can we solve this problem?
Well, looking at the implementation of saveHierarchyState and dispatchSaveInstanceState, we can observe that every state is stored in one container and is shared for the entire view hierarchy. Let's draw up the current hierarchy:
As you can see, the tags #+id/customViewSwitch and #+id/customViewEditText and are repeated, so the generated sparse array saves the children of #+id/switch1, then #+id/switch2, and finally #+id/switch3.
From the documentation, we are able to learn that SparseArray is:
SparseArray maps integers to Objects and, unlike a normal array of
Objects, its indices can contain gaps. SparseArray is intended to be
more memory-efficient than a HashMap , because it avoids auto-boxing
keys and its data structure doesn’t rely on an extra entry object for
each mapping.
Let's check how the container behaves when we pass the same key again:
val array = SparseArray()
array.put(1, "test1")
array.put(1, "test2")
array.put(1, "test3")
Log.i("TAG", array.toString())
The result is:
I/TAG: {1=test3}
So new values overwrite the previous ones. Considering the fact that the key in the previous case is ID, this explains why each of the views received the state of the last child.
Here is one solution to this problem.
At the beginning, we should overwrite 2 callbacks: dispatchSaveInstanceState and dispatchRestoreInstanceState:
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
This way, super.onSaveInstanceState() will only return the super state, avoiding children views.
We are now ready to handle the state inside onSaveInstanceState and onRestoreInstanceState. Let’s start with the saving process:
override fun onSaveInstanceState(): Parcelable? {
return saveInstanceState(super.onSaveInstanceState())
}
Now It’s time to restore the state:
override fun onRestoreInstanceState(state: Parcelable?) {
super.onRestoreInstanceState(restoreInstanceState(state))
}
And here are the Viewgroup extenstion functions
fun ViewGroup.saveChildViewStates(): SparseArray<Parcelable> {
val childViewStates = SparseArray<Parcelable>()
children.forEach { child -> child.saveHierarchyState(childViewStates) }
return childViewStates
}
fun ViewGroup.restoreChildViewStates(childViewStates: SparseArray<Parcelable>) {
children.forEach { child -> child.restoreHierarchyState(childViewStates) }
}
fun ViewGroup.saveInstanceState(state: Parcelable?): Parcelable? {
return Bundle().apply {
putParcelable("SUPER_STATE_KEY", state)
putSparseParcelableArray("SPARSE_STATE_KEY", saveChildViewStates())
}
}
fun ViewGroup.restoreInstanceState(state: Parcelable?): Parcelable? {
var newState = state
if (newState is Bundle) {
val childrenState = newState.getSparseParcelableArray<Parcelable>("SPARSE_STATE_KEY")
childrenState?.let { restoreChildViewStates(it) }
newState = newState.getParcelable("SUPER_STATE_KEY")
}
return newState
}
In this example, we call the restoreHierarchyState function for each child to which I pass the previously saved SparseArray. Let’s check how the hierarchy looks now:
And now here is the result:
Everything works well! This is one of the ways to save the state of your own view, but there is another interesting solution using the BaseSavedState class. You can learn more from the original blog post by Marcin Stramowski

Just mark 1 checkbox of each question and save answer of each position - kotlin

Good morning, community, I have the following case, I have 1 list of questions with their respective YES/NO answers, which are the checkboxes, what is complicating me is how I can apply 1 validation that only allows marking 1 answer (yes or no), in turn save that answer with its respective position and then save it in a DB.
this is my adapter(preusoadapter.kt)
class preusoadapter(
private val context : Context,
private val listpreguntaspreuso: ArrayList<epreguntas>
) : RecyclerView.Adapter<preusoadapter.PreUsoViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreUsoViewHolder {
val layoutInflater = LayoutInflater.from(context)
return PreUsoViewHolder(layoutInflater.inflate(R.layout.activity_estructura_listapreuso, parent, false)
)
}
override fun onBindViewHolder(holder: PreUsoViewHolder, position: Int) {
val item = listpreguntaspreuso[position]
holder.render(item)
holder.displayChecked(item.answer)
if (position == 2) {
holder.displayAnswers(setOf(Answer.IVSS, Answer.DSS))
} else if (position == 6) {
holder.displayAnswers(setOf(Answer.FRESERV, Answer.FREDMANO))
} else if (position == 14) {
holder.displayAnswers(setOf(Answer.NA, Answer.SI, Answer.NO))
} else if (position == 18) {
holder.displayAnswers(setOf(Answer.NA, Answer.SI, Answer.NO))
} else if (position == 22) {
holder.displayAnswers(setOf(Answer.NA, Answer.SI, Answer.NO))
} else if (position == 25) {
holder.displayAnswers(setOf(Answer.NA, Answer.SI, Answer.NO))
}
}
override fun getItemCount(): Int = listpreguntaspreuso.size
//CLASE INTERNA PREUSOVIEWHOLDER//
inner class PreUsoViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val binding = ActivityEstructuraListapreusoBinding.bind(view)
private val idpregunta = view.findViewById<TextView>(R.id.txtidpregunta)
private val numeropregunta = view.findViewById<TextView>(R.id.txtnumeropregunta)
private val pregunta = view.findViewById<TextView>(R.id.txtpreguntas)
private val imgestado = view.findViewById<ImageView>(R.id.icosemaforo)
fun render (epreguntas: epreguntas){
idpregunta.text = epreguntas.id_pregunta
numeropregunta.text = epreguntas.num_pregunta
pregunta.text = epreguntas.pregunta
Glide.with(imgestado.context).load(epreguntas.icono_estado).into(imgestado)
}
private val checkboxAnswers = mapOf(
binding.chbksi to Answer.SI,
binding.chbkno to Answer.NO,
binding.chbkna to Answer.NA,
binding.chbkIVSS to Answer.IVSS,
binding.chbkDSS to Answer.DSS,
binding.chbkFSERV to Answer.FRESERV,
binding.chbkfmano to Answer.FREDMANO
)
init {
// set the listener on all the checkboxes
checkboxAnswers.keys.forEach { checkbox ->
checkbox.setOnClickListener { handleCheckboxClick(checkbox) }
}
}
// A function that handles all the checkboxes
private fun handleCheckboxClick(checkbox: CheckBox) {
// get the item for the position the VH is displaying
val item = listpreguntaspreuso[adapterPosition]
// update the item's checked state with the Answer associated with this checkbox
// If it's just been -unchecked-, then that means nothing is checked
checkboxAnswers[checkbox]?.let { answer ->
item.answer = if (!checkbox.isChecked) null else answer
// remember to notify the adapter (so it can redisplay and uncheck any other boxes)
notifyItemChanged(adapterPosition)
}
}
fun displayChecked(answer: Answer?) {
// set the checked state for all the boxes, checked if it matches the answer
// and unchecked otherwise.
// Setting every box either way clears any old state from the last displayed item
checkboxAnswers.forEach { (checkbox, answerType) ->
checkbox.isChecked = answerType == answer
}
}
fun displayAnswers(answers: Collection<Answer>) {
// iterate over each checkbox/answer pair, hiding or displaying as appropriate
checkboxAnswers.forEach { (checkbox, answerType) ->
checkbox.visibility = if (answerType in answers) View.VISIBLE else View.GONE
}
}
}
}
and this is my class epreguntas.kt
class epreguntas(
var id_pregunta: String,
var num_pregunta: String,
var pregunta : String,
var icono_estado: String,
var checkvalor: Boolean = false,
var answer: Answer? = null
) {
}
this is my structure i use for my recylcview
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:animateLayoutChanges="true"
app:cardCornerRadius="2dp"
app:cardElevation="4dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="#+id/icosemaforo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="#drawable/ic_android" />
</RelativeLayout>
<LinearLayout
android:id="#+id/contenedor_categoria1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:orientation="vertical"
>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:id="#+id/txtnumeropregunta"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_alignParentTop="true"
android:text="N°"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="#+id/txtpreguntas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="20dp"
android:layout_toStartOf="#+id/contenedorcheck"
android:textSize="14sp"
android:layout_toEndOf="#+id/txtnumeropregunta"
android:textStyle="bold"
android:text="Preguntas" />
<LinearLayout
android:id="#+id/contenedorcheck"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:paddingEnd="20dp"
>
<CheckBox
android:id="#+id/chbksi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="#string/check_si" />
<CheckBox
android:id="#+id/chbkIVSS"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:visibility="gone"
android:text="#string/check_IVSS" />
<CheckBox
android:id="#+id/chbkFSERV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:visibility="gone"
android:text="#string/check_freserv" />
<CheckBox
android:id="#+id/chbkno"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="#string/check_no" />
<CheckBox
android:id="#+id/chbkfmano"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginEnd="10dp"
android:text="#string/check_fredmano" />
<CheckBox
android:id="#+id/chbkDSS"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginEnd="10dp"
android:text="#string/check_DSS" />
<CheckBox
android:id="#+id/chbkna"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginEnd="10dp"
android:text="#string/check_na" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
It complies with selecting 1 box and unchecking the other, but I realize that when I check the box for question 1 (yes) another question overlaps and my question 1 is hidden, and I see that in some questions the YES/NO/ NA up to IVSS and DSS
example
Like Tenfour04 says, RadioButtons in a RadioGroup would handle the "only one thing can be selected" functionality. (You'd need a different listener to handle its button id has been selected callbacks.)
But since you're already storing the checked state and displaying it, you can handle that yourself - you just need a way to ensure when one checkbox is checked, the others are stored as unchecked.
An easy way to do that is to store an Int which represents the checkbox that's selected. Say 0 for the first, 1 for the second, and a negative number for none. Because each number represents one checked item, by changing the number you're "unchecking" the others.
If you want to store that in your data object (like with checkvaloryes) you can just do:
class epreguntas(
var id_pregunta: String,
...
var checkvaloryes: Boolean = false,
var id_answer: Int
)
Then your checkbox click listeners just have to store the appropriate value, and in onBindHolder you enable the selected checkbox and disable all the others.
This is basically what using a RadioGroup involves too - you get a button ID when the selection changes (-1 for no selection), you store that, and when you display it you fetch that stored ID and set it on the RadioGroup.
The automatic deselection of other buttons (in single-selection mode) is nice and convenient, but setting the current button in onBindViewHolder will trigger the OnCheckedChangedListener and you can get stuck in a loop, so you'd need to avoid that. One way is to only update your stored value (and notify the adapter of the change) if the current selection ID is different to the stored one.
But you can also use checkboxes and click listeners like you already are, you just have to handle their state yourself. Here's a bit of a long explanation of one way to do it, but it's partly about solving other problems you're probably going to run into.
I'm gonna complicate things a bit, because I feel like it will help you out once you understand what's happening - you have a few things to wrangle here. I'm just posting this as one way to approach that store and display problem.
First, I think it would be a good idea to define your options somewhere. Since you have a fixed set of checkboxes, there's a fixed set of answers, right? You could define those with an enum:
enum class Answer {
SI, NO, IVSS, FRESERV, FREDMANO, DSS, NA
}
And you could store that in your data:
class epreguntas(
...
var answer: Answer? = null // using null for 'no answer selected'
)
(You can use that enum elsewhere in your app too - it means your answer data is in a useful data structure, and it's not tied to some detail about your list display, like what order the checkboxes happen to be added in the layout. If you need to turn these enum constants into an Int, e.g. for storage, you can use their ordinal property)
Now you can connect those responses to the checkboxes that represent them. We could do that in the ViewHolder class:
class PreUsoViewHolder(view: View) : RecyclerView.ViewHolder(view) {
...
// create an answer type lookup for each checkbox in the ViewHolder instance
// I'm going to use your binding object since you have it!
val checkboxAnswers = mapOf(
binding.chbksi to SI,
binding.chbkno to NO,
...
)
That checkboxAnswers map acts as two things - it's a lookup that links each CheckBox in the layout to a specific answer type, and the keys act as a collection of all your CheckBox views, so you can easily do things to all of them together.
Now you can create a click listener that checks which View was clicked, get the matching Answer, and set it:
// I've made this an -inner- class, and it need to be nested inside your Adapter class
// This gives the ViewHolder access to stuff inside the adapter, i.e. listpreguntaspreuso
inner class PreUsoViewHolder(view: View) : RecyclerView.ViewHolder(view) {
init {
...
// set the listener on all the checkboxes
checkboxAnswers.keys.forEach { checkbox ->
checkbox.setOnClickListener { handleCheckboxClick(checkbox) }
}
// A function that handles all the checkboxes
private fun handleCheckboxClick(checkbox: CheckBox) {
// get the item for the position the VH is displaying
val item = listpreguntaspreuso[adapterPosition]
// update the item's checked state with the Answer associated with this checkbox
// If it's just been -unchecked-, then that means nothing is checked
checkboxAnswers[checkbox]?.let { answer ->
item.answer = if (!checkbox.isChecked) null else answer
// remember to notify the adapter (so it can redisplay and uncheck any other boxes)
notifyItemChanged(adapterPosition)
}
}
This relies on you using a click listener, not a checkedChanged listener, because setting checked state in onBindViewHolder (when you're clearing checkboxes) will trigger that checkedChanged listener. A click listener only fires when the user is the one checking or unchecking a box.
So now you have a click listener that sets the appropriate Answer value for an item. To display it, we could put another function in the ViewHolder:
fun displayChecked(answer: Answer?) {
// set the checked state for all the boxes, checked if it matches the answer
// and unchecked otherwise.
// Setting every box either way clears any old state from the last displayed item
checkboxAnswers.forEach { (checkbox, answerType) ->
checkbox.isChecked = answerType == answer
}
}
And now you can call that from onBindViewHolder:
override fun onBindViewHolder(holder: PreUsoViewHolder, position: Int) {
val item = listpreguntaspreuso[position]
holder.render(item)
holder.displayChecked(item.answer)
The other reason for doing things this way, is I think your code to make stuff visible/invisible is broken:
else if (position == 14){
holder.itemView.chbkna.visibility = View.VISIBLE
}
This kind of thing won't work - all you're doing is saying "for item 14, make this box visible" - it says nothing about which of the other boxes should be visible, and which should be hidden. You'll have stuff randomly shown or hidden depending on which item happened to be displayed in that ViewHolder before. You need to explicitly say what should be displayed, every time onBindViewHolder runs.
You can do that with a similar function to the displayChecked one we just wrote:
inner class PreUsoViewHolder(view: View) : RecyclerView.ViewHolder(view) {
...
// provide a list of the answers that should be shown
fun displayAnswers(answers: Collection<Answer>) {
// iterate over each checkbox/answer pair, hiding or displaying as appropriate
checkboxAnswers.forEach { (checkbox, answerType) ->
checkbox.visibility = if (answerType in answers) VISIBLE else GONE
}
}
Now you can easily update your displayed boxes in onBindViewHolder:
else if (position == 14){
holder.displayAnswers(setOf(NA, SI, NO))
}
And even better, you could create groups of answer types associated with the question in the data. So instead of hardcoding by position, you can just pull the required types out of the item itself
class epreguntas(
...
answerTypes: Set<Answer>
// onBindViewHolder
holder.displayAnswers(item.answerTypes)
You could even create preset lists for different types of question, like val yesNo = setOf(SI, NO) (or an enum) and reuse those when defining your questions - there are probably only a few combos you're using anyway!
I hope that wasn't too complicated, and the organisation ideas (and the benefits they can give you) make sense. A RadioGroup is simpler, but with the other stuff you'll probably have to deal with, I feel like this is a useful general approach

Android Studio - Same onClick function for multiple views

So I'd like to make a function that changes the tint of the view that is calling the function.
I'm pretty sure I got the tint part down I'm just not quite sure how or where I need to define this function so that I can either select it as the onClick method in the built-in properties menu or I can reference it in the xml file(preferably the former).
Right now I have the function in the MainActivity.kt file inside the class and I selected the function on all the different views in the properties menu but when I run the app and actually click on of these views I get a crash saying "Could not find method in parent or ancestor Context for android:onClick attribute"
I would really appreciate some help with this, thanks in advance!
You can set the same on click listener to multiple views.
val tintChanger = View.OnClickListener { view ->
println("View with id=${view.id} clicked")
changeTintOf(view as ImageView)
}
imageViewOne.setOnClickListener(tintChanger)
imageViewTwo.setOnClickListener(tintChanger)
imageViewThree.setOnClickListener(tintChanger)
How to set on click listener to views without knowing their ids?
val imageContainerLayout = findViewById<LinearLayout>(R.id.imageContainer)
// val imageContainerLayout = binding.imageContainer
imageContainerLayout.children.forEach {
it.setOnClickListener(tintChanger)
}
// xml
<LinearLayout
android:id="#+id/imageContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<ImageView ... /> // without android:id set
<ImageView ... /> // without android:id set
<ImageView ... /> // without android:id set
</LinearLayout>
Not preferred way nowadays but if you want to set a click listener on your view by xml, your activity should contain a public method changeTintOnClick with an argument view: View.
// MainActivity.kt
fun changeTintOnClick(view: View) {
println("View click listener set by XML")
println("View clickView with id=${view.id} clicked")
changeTintOf(view as ImageView)
}
private fun changeTintOf(view: ImageView) {
// your implementation for tint
}
<ImageView ...
android:onClick="changeTintOnClick"
/>
One way you can pull this of is placing an OnClickListener extension on your main activity class and set all of the views with an id and tag to reference later.
class MainActivity : AppCompatActivity(), View.OnClickListener{ /*Extend class to conformalso to View.OnClickListsner*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
//Let's say you have two or more views to use.
//You want to include the same listener to all opf the same buttons you want to use
findViewById<Button>(R.id.myelementone).setOnClickListener(this)
findViewById<Button>(R.id.myelementTwo).setOnClickListener(this)
override fun onClick(v: View) {
switch(v.tag){
case "myelementonetag":
//Do something
break;
case "myelementtwotag"
//Do something else
break;
default:
//If no tags match the clicked item
break;
}
}
}
The only thing you really need to do in XML is set the id of the element and the tag of the elements like this (using onClick in XML is no longer best practice and advised that it should not be used anymore. Dont forget to keep they styling for you own button!):
<Button
android:id="#+id/myelementone"
tag="myelementonetag"/>
<Button
android:id="#+id/myelementone"
tag="myelementonetag"/>
If you need a little more on how this works, here is another StackOverflow question that was answered with all best ways to implement click functions: Android - How to achieve setOnClickListener in Kotlin?

BindingAdapter in Android

I am following this course about RecyclerView and Databinding.
I've read What is the use of binding adapter in Android?.
Other than make custom/more complex setter, what is the benefit of using BindingAdapter over the "normal" way ? Is there a gain in performance ?
Version1:
xml:
<TextView
android:id="#+id/sleep_length"
android:layout_width="0dp"
android:layout_height="20dp"
...
tools:text="Wednesday" />
Adapter:
fun bind(item: SleepNight) {
val res = itemView.context.resources
sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
}
Version2 (DataBinding):
xml:
<TextView
android:id="#+id/sleep_length"
android:layout_width="0dp"
android:layout_height="20dp"
app:sleepDurationFormatted="#{sleep}"
...
tools:text="Wednesday" />
Adapter:
fun bind(item: SleepNight) {
binding.sleep = item
}
BindingUtils:
#BindingAdapter("sleepDurationFormatted")
fun TextView.setSleepDurationFormatted(item: SleepNight){
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)
}
Binding Adapter gives you a pretty good feature in customization of a view.
Seems weird!.
First, Suppose you have an ImageView that shows the country flag.
Accept: Country code(String)
Action: Show the flag of the country, if the country code is null, make ImageView GONE.
#BindingAdapter({"android:setFlag"})
public static void setFlagImageView(ImageView imageView, String currencyCode) {
Context context = imageView.getContext();
if (currencyCode != null) {
try {
Drawable d = Drawable.createFromStream(context.getAssets().open("flags/"+currencyCode.toLowerCase()+".png"), null);
imageView.setImageDrawable(d);
} catch (IOException e) {
e.printStackTrace();
}
}
else{
imageView.setVisibility(View.GONE);
}
}
So now you can reuse this BindinAdapter any elsewhere.
People who loves DataBinding, see that they can reduce amount of code and write some logic in xml. Instead of helper methods.
Second, By databinding, you will ignore findViewById, because a generated file would be created for you.
Regarding performance, I don't find in official documentation any indicate that BindingAdapter increases performance.

Android - ResourcesNotFoundException

I am passing my custom object sub which has a color property from an activity to another and retrieving it like this:
val intent = this.intent
val bundle = intent.extras
sub = bundle.getParcelable("selected")
then, when a button is pressed a color picker shows up and lets me select a color, I have this method that listens for the color selection:
override fun onColorSelected(dialogId: Int, color: Int) {
sub.color = color
createsub_rel.backgroundColor = color
}
as you can see the color is returned as an Int.
The Exceptions happens in the onBindViewHolder() of my RecyclerView, specifically on this line:
viewHolder.relativeLayout.setBackgroundColor(mContext.getColor(sub.color))
the log states:
android.content.res.Resources$NotFoundException: Resource ID #0x7fff9800
I have debugged it and sub.color is actually the expected value, I have looked here on SO for a solution, specifically on this, but I couldn't find any working answer.

Categories

Resources