Populate multiple fields in one line in Kotlin - android

I would like to know if it's possible to populate multiple fields in one line in Kotlin (just for cleaner code)
My Code:
val evh = ExampleViewHolder(binding.root)
evh.mImageView = binding.myImageView
evh.mTextView1 = binding.text1
evh.mTextView2 = binding.text2
I would like to achieve something like this:
(evh.mImageView, evh.mTextView1, evh.mTextView2) = (binding.myImageView, binding.text1, binding.text2)
Is this somewhat possible?

is this somewhat possible?
No, you can't set properties* in such a way in Kotlin.
If you'd like to initialize or change values on some properties in a cleaner way, you can use one of the scope functions:
The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object. When you call such a function on an object with a lambda expression provided, it forms a temporary scope. In this scope, you can access the object without its name. Such functions are called scope functions. There are five of them: let, run, with, apply, and also.
In this particular case it seems like apply is the best fit:
val evh = ExampleViewHolder(binding.root).apply {
mImageView = binding.myImageView
mTextView1 = binding.text1
mTextView2 = binding.text2
}
*note that similar inline syntax is valid in Kotlin and it's used in destructing declarations:
val (first, second) = listOf("firstValue", "secondValue")

You could also pass views as arguments to ExampleViewHolder, like this:
class ExampleViewHolder(
val rootView: View, val imageView: ImageView,
val textView1: TextView, val textView2: TextView
) : RecyclerView.ViewHolder(rootView) {...}
And then instantiate the ViewHolder using the apply scope function:
val viewHolder = binding.run {
ExampleViewHolder(root, myImageView, text1, text2)
}

I found this post which says it's possible if you put all views in a data class:
data class ExampleViewHolder(val mImageView: ImageView, val mTextView: TextView, val mTextView2: TextView, val binding: Binding) : RecyclerView.ViewHolder(binding.root)
and then do something like this:
val (mImageView, mTextView1, mTextView2, binding) = ExampleViewHolder(binding.myImageView, binding.text1, binding.text2, binding)
Alternatively you can always put semi-columns in between assignments to one-line it, but it does not get more readable imo:
evh.mImageView = binding.myImageView; evh.mTextView1 = binding.text1; evh.mTextView2 = binding.text2
I would just assign the fields inside your ExampleViewHolder:
class ExampleViewHolder(binding: Binding) : RecyclerView.ViewHolder(binding.root) {
val mImageView = binding.myImageView
val mTextView = binding.text1
val mTextView2 = binding.text2
}
Or even let the ExampleViewHolder do the binding:
class ExampleViewHolder(val binding: Binding) : RecyclerView.ViewHolder(binding.root) {
fun bindTo(item: Any) {
binding.myImageView.visibility = View.GONE
}
}

If an object has already been created, to change several fields you can use scope function "run", like:
evh.run {
mImageView = binding.myImageView
mTextView1 = binding.text1
mTextView2 = binding.text2
}

Related

Kotlin Change ViewText with an ID provided by a String

Goal: To get a ViewText resource and edit it from an activity, using a mutable string (because then the string can be changed to alter other ViewTexts in the same function).
Context: I'm making a grid using TableRows and TextViews that can be altered to form a sort of map that can be generated from an array.
Issue: The binding command does not recognise strings. See my comment "PROBLEM HERE".
Tried: getResources.getIdentifier but I've been told that reduces performance drastically.
An excerpt from gridmap.xml
<TextView
android:id="#+id/cell1"/>
GridMap.kt
package com.example.arandomadventure
import android.R
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.arandomadventure.databinding.GridmapBinding
class GridMap : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//sets the binding and assigns it to view
val binding: GridmapBinding = GridmapBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
//creates a string variable
var cellID = "cell1"
//uses binding to set the background colour to teal
binding.cellID.setBackgroundResource(R.color.teal_200) //<- PROBLEM HERE (Unresolved Reference)
//getResources.getIdentifier is not an option as it degrades performance on a larger scale
}
}
A binding object is just an autogenerated class, whose class members are defined by the views in your layout XML. You can't add or access a field on a class with the syntax you showed - binding classes are no different from any other class. If you wanted to be able to access them by name, you could load them into a map
val viewMap = mapOf(
"cell1" to binding.cell1,
"cell2" to binding.cell2,
"cell3" to binding.cell3
)
then you can use the map to access them by name
var cellID = "cell1"
viewMap[cellID].setBackgroundResource(R.color.teal_200)
If you want the map to be a class member, you can set it like this
private lateinit var viewMap: Map<String,View>
override fun onCreate(savedInstanceState: Bundle?) {
//...
viewMap = mapOf(
"cell1" to binding.cell1,
"cell2" to binding.cell2,
"cell3" to binding.cell3
)
}
If your layout has hundreds of views and this becomes cumbersome, you may want to consider adding the views programmatically instead.
Edit
If you want to do this a more ugly, but more automatic way you can use reflection. To do this you need to add this gradle dependency:
implementation "org.jetbrains.kotlin:kotlin-reflect:1.7.0"
then you can build up the map programmatically with all views in the binding.
val viewMap = mutableMapOf<String,View>()
GridmapBinding::class.members.forEach {
try {
val view = it.call(binding) as? View
view?.let { v ->
viewMap[it.name] = v
}
}
catch(e: Exception) {
// skip things that can't be called
}
}
Or you can use this to call a method (keep in mind this will throw if no such class member exists):
var cellID = "cell1"
val view = GridmapBinding::class.members.filter { it.name == cellID }[0].call(binding)

How can I iterate over all views known to the data binder?

I have three TextInputEditText views in my layout where the user can type in specific information.
On the click of a Button this information is stored in my database.
After the user clicks this Button, I want to clear all TextInputEditText fields.
Right now, I am doing this by hardcoding:
private fun clearAllEditTextFields() {
Timber.d("clearAllEditTextFields: called")
binding.bookTitleEditText.text = null
binding.bookAuthorEditText.text = null
binding.bookPageCountEditText.text = null
}
Since this is bad, I would like to use a dynamic for each loop to identify all views of type TextInputEditText known to binding and clear their content:
private fun clearAllEditTextFields() {
Timber.d("clearAllEditTextFields: called")
for (view in binding.views) {
if (view is TextInputEditText) {
view.text = null
}
}
Unfortunately, there is no such field binding.views.
Is there still a way to achieve this or something with the same properties?
What I have tried so far
I have used a BindingAdapter. In my Util class, where all my extension functions go, I have created an EditText extension function clearText annotated as BindingAdapter and JvmStatic:
#JvmStatic
#BindingAdapter("clearText")
fun EditText.clearText(#NotNull shouldClear: Boolean) {
Timber.d("clearText: called")
if (shouldClear) text = null
}
In XML:
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/book_title_edit_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeActionId="100"
android:imeOptions="actionNext"
android:inputType="text"
android:text="#={viewModel.bookTitle}"
app:clearText="#{viewModel.clearAllEditTextFields}"
/>
In my ViewModel class, I have created a var clearAllEditTextFields = false which is modified in the clearAllEditTextFields() function which gets called inside my ViewModel:
...
var clearAllEditTextFields = false
clearAllEditTextFields()
...
private fun clearAllEditTextFields() {
Timber.d("clearAllEditTextFields: called")
clearAllEditTextFields = true
}
According to Logcat, my extension function is called when my ViewModel is initialized. However, when clearAllEditTextFields() gets called, it does not trigger a new call to the extension function.
A simple for loop doesn't exist to loop over the views in the binding object and you can try the following to keep your code conscice.
Scope Functions
binding.apply{
bookTitleEditText.text = null
bookAuthorEditText.text = null
bookPageCountEditText.text = null
}
scope functions are a good go iff there are few views and we end up with quite a boiler-plate code if the number of views is large, in which cases I think Binding-Adapter would be a good choice
#BindingAdapter("clear_text")
fun EditText.clearText(shouldClear : Boolean?){
shouldClear?.apply{
if(shouldClear)
text = null
}
}
ViewModel
private val _shouldClear = MutableLiveData<Boolean>()
val shouldClear : LiveData<Boolean>
get() = _shouldClear
fun setClearStatus(status : Boolean){
_shouldClear.value = status
}
//since clearing a text is an event and not state, reset the clear_status once it's done
fun resetClearStatus(){
_shouldClear.value = nul
}
XML
<EditText
......
app:clear_text = "#{yourViewModel.shouldClear}"
...... />
ActivityClass
...
binding.lifecycleOwner = this
...
private fun clearAllEditTextFields() {
yourViewModel.setClearStatus(true)
yourViewModel.resetClearStatus()
}
Edit:
add binding.lifecycleOwner = this in your activity class and its used for observing LiveData with data binding. The view will observe for text changes at runtime.
Create a linearlayout (or similar) called, for example, text_fields_linear layout enclosing all of your textfields. then do:
private fun clearAllEditTextFields() {
for (item in binding.textFieldsLinearLayout) {
item.text = null
}
}

Inconsistencies with RecyclerViews, ViewHolders and onBindViewHolder

I'm learning/using RecyclerViews and while my app works (at the moment!), there are two things that I don't understand.
Here are my ViewHolder declarations:
class AAAViewHolder ( view: View, var aaa: AAA? = null) : RecyclerView.ViewHolder (view) {...}
class BBBViewHolder (val view: View, var bbb: BBB? = null) : RecyclerView.ViewHolder (view) {...}
class CCCViewHolder ( view: View, var ccc: CCC? = null) : RecyclerView.ViewHolder (view) {...}
Why does BBBViewHolder have the extra val? If I remove it, then I get an "Unresolved reference: view" compiler error in onBindViewHolder in the ViewAdapter class. Why? And, if I *add the val declaration to AAA and CCC, Android Studio tells me that it's not needed and offers to remove it for me.
Next, there's something odd about the onBindViewHolder functions.
AAAListAdapter.kt (not showing getItemCount or onCreateViewHolder):
class AAAListAdapter : RecyclerView.Adapter<AAAViewHolder>() {
override fun onBindViewHolder(holder: AAAViewHolder, position: Int) {
val aaa = aaaList[position]
holder.itemView.aTextView.text = "AAA"
holder.aaa = aaa
}
}
BBBListAdapter.kt
class BBBListAdapter : RecyclerView.Adapter<BBBViewHolder>() {
override fun onBindViewHolder(holder: BBBViewHolder, position: Int) {
val bbb = bbbList[position]
holder.view.bTextView.text = "BBB"
holder.bbb = bbb
}
}
CCCListAdapter.kt
class CCCListAdapter : RecyclerView.Adapter<CCCViewHolder>() {
override fun onBindViewHolder(holder: CCCViewHolder, position: Int) {
val ccc = cccList[position]
holder.itemView.cTextView.text = "CCC"
holder.ccc = ccc
}
}
The code is almost identical, except why does BBBListAdapter reference holder.view, while the other two reference holder.itemView? Where are those properties declared? Can I control that? I'd much prefer them to be the same.
Seeing how A & C act the same but B is different, I'm guessing the two questions are related, but I don't know.
Firstly you declare val/var inside constructor to use those values somewhere in class without declaring or intializing it anywhere in your class. Let take in example, i want a list in adapter I'll pass it in adapter and in adapter I won't use val/var then and I can't use that unless I create a variable before hand and initialise it inside its default constructor.
class A() {
lateinit var view : View
constructor(view : View) {
this.view = view
}
view.textView.text = "Redundant Code"
}
Now you could have reduced this just by declaring it inside constructor itself.
class A(val view : View) {
view.textView.text = "Easy way"
}
Now coming to your use case, viewholder A and C are identical, and B has view is declared and you are using it, but from the code in adapter I don't think it is necessary, the same logic could have been used in Adapter B, holder.itemView.something, holder.itemView is ultimately is the view object which you're using in A and C, so val view is not need for that particular case.
If you're using it somewhere, then add the whole code, there I might be able to help you out why ViewHolder B is different. But from what you have posted, there is no need for using val inside constructor.

TextView text changes but does not update in layout (Kotlin - Android)

TextView text changes but does not update in layout. I tried every method I could find but nothing worked. I have a very basic application with a single activity and 3 layouts*.
*This is the first app I make so I tought it would have been simpler this way
The main problems I am facing are two: almost all the informations around are old and in java, and my textView text does not change.. The app is a simple Rock-Paper-Scissor game I'm trying to make as an exercise.
The textViews.text values get updated but the layout always shows the same text...
I have no idea what could be the problem. I am also struggling to understand exactly how all of this is working exactly...like InflateLayout, Context and Android in general. I do not understand much from android's reference.
THERE IS NO INFLATE(), POSTINFLATE(), FORCELAYOUT(), VISIBILITY TOGGLES BECAUSE NONE OF THEM WORKED :(
Excerpt of the code
class MainActivity : AppCompatActivity() {
lateinit var TITLE:TextView
lateinit var PARAGRAPH:TextView
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val InflaterInitializer = LayoutInflater.from(applicationContext) as LayoutInflater
val inflater = InflaterInitializer.inflate(R.layout.activity_2, null, false)
TITLE= inflater.findViewById(R.id.title) as TextView
PARAGRAPH= inflater.findViewById(R.id.paragraph) as TextView
}
There are three functions like this:
fun FUNCTION(v :View) {
val userChoice = "XXX"
val computerChoice = getComputerChoice()
if (userChoice == computerChoice) {
FUNCTION_2(computerChoice)
} else {
runOnUiThread {
TITLE.text =
if (computerChoice == "YYY") getString(R.string.YOU_WON) else getString(R.string.YOU_LOSE);
PARAGRAPH.text = getString(R.string.STRING, computerChoice)
}
}; resultScreen()
}
Function_2...
private fun FUNCTION_2(cc :String) {
runOnUiThread {
TITLE.text = getString(R.string.STRING)
PARAGRAPH.text = getString(R.string.STRING, cc)
}; resultScreen()
}
resultScreen() is just a call to setContentView(LAYOUT)
Here's a video of the app and the update problem:
https://imgur.com/a/iWCRMkq
Code complete here: https://github.com/noiwyr/MorraCinese
EDIT
Unfortunately none of the answers actually worked as I hoped, however redesigning the app and using multiple activities with some tweaks solved the issue. You may find the new code in the github repo.
However I would be curious to know if there is a working solution for this question :)
By calling InflaterInitializer.inflate(R.layout.activity_2, null, false) you inflate a new view hierarchy from the specified xml resource, which is not attached to any of your views (these new views are not shown on your screen). Then you found text views from that new view hierarchy and changed their titles.
So, your onCreate method have to look like this:
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_2)
TITLE = findViewById(R.id.title)
PARAGRAPH = findViewById(R.id.paragraph)
}
Also, it's redundant to use methods runOnUiThread() (your code already runs on Ui thread) and resultScreen().
You no need anything , you creat over code no problem I suggest you
val InflaterInitializer = LayoutInflater.from(applicationContext) as LayoutInflater val inflater = InflaterInitializer.inflate(R.layout.activity_outcome, null, false)
Comment this above code no need in kotlin
motivoRisultato.text = getString(R.string.scelta_pc, computerChoice)
Simpaly make this type of code
There are quite a few errors in your code, so I'm going to break down the answer with your code. Do find the Comments inline
class MainActivity : AppCompatActivity() {
/**
* First, Follow conventions for naming variables, they are usually in camelcase for variables and functions, Capitalized for Constants.
* Second, lateinit is used to defer the initialization of a variable, for views, such as
* TextView's, you could use the Kotlin Synthentic library which automatically references the Views of your layout.
*/
lateinit var TITLE:TextView
lateinit var PARAGRAPH:TextView
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
/**
* Set content view, internally set's the layout file after inflation using the Activity context. Which means, that you do not
* need to specifically inflate the view.
*/
setContentView(R.layout.activity_main)
/**
* This is the reason why your layout doesn't know refresh, what you're doing here is inflating another layout, but not setting it to your activity.
* This is not required as explained above
*/
val InflaterInitializer = LayoutInflater.from(applicationContext) as LayoutInflater
/**
* Inflater inflates a View Object. one would use this approach if they were programatically adding Views
*/
val inflater = InflaterInitializer.inflate(R.layout.activity_2, null, false)
/**
* the below views are pointing to a reference of TextView for the non visible inflated view. Which is the reason why the text is not updated.
*/
TITLE= inflater.findViewById(R.id.title) as TextView
PARAGRAPH= inflater.findViewById(R.id.paragraph) as TextView
}
}
Here's the code to make things work
class MainActivity : AppCompatActivity() {
private var title:TextView? = null
private var paragraph:TextView? = null
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
title= inflater.findViewById(R.id.title) as TextView
paragraph= inflater.findViewById(R.id.paragraph) as TextView
}
fun function(v :View) {
val userChoice = "XXX"
val computerChoice = getComputerChoice()
if (userChoice == computerChoice) {
function2(computerChoice)
} else {
title.text = if (computerChoice == "YYY") getString(R.string.YOU_WON) else getString(R.string.YOU_LOSE);
paragraph.text = getString(R.string.STRING, computerChoice)
}
resultScreen()
}
private fun function2(cc :String) {
title.text = getString(R.string.STRING)
paragraph.text = getString(R.string.STRING, cc)
resultScreen()
}
}
If your use case is to show different screens, look at starting more than one Activity and transitioning between them using Intents

Kotlin: Read Only access of Immutable Type to an Internal Variable of a Mutable Type

While learning ViewModels in Android, a problem has arisen that feels like Kotlin was meant to solve. In the code below, we can see that MutableLiveData values are being use to edit values and indicators. However, we do not want these mutable values to be exposed to anything else, specifically members of an Android lifecycle. We DO want Android Lifecycle members to have access to read values but not set them. Therefore, the 3 exposed functions, displayed below, are of the LiveData<> immutable type.
Is there an easier or more concise way to expose read only values that can be edited internally? This seems like what Kotlin was made to avoid: boilerplate verbosity.
class HomeListViewModel: ViewModel(){
//Private mutable data
private val repositories = MutableLiveData<List<Repo>>()
private val repoLoadError = MutableLiveData<Boolean>()
private val loading = MutableLiveData<Boolean>()
//Exposed uneditable LIveData
fun getRepositories():LiveData<List<Repo>> = repositories
fun getLoadError(): LiveData<Boolean> = repoLoadError
fun getLoadingStatuses(): LiveData<Boolean> = loading
init{...//Do some stuff to MutableLiveData<>
}
}
A non-Android scenario that might be similar is:
class ImmutableAccessExample{
private val theThingToBeEditedInternally = mutableListOf<String>()
fun theThingToBeAccessedPublicly(): List<String> = theThingToBeEditedInternally
init {
theThingToBeEditedInternally.add(0, "something")
}
}
I don't know if it is possible to avoid the verbosity. But, I've seen that before and it is usually declared as a property.
private val _repositories = MutableLiveData<List<Repo>>()
val repositories : LiveData<List<Repo>>
get() = _repositories
This is the convention, see the doc here in Names for backing properties
If a class has two properties which are conceptually the same but one is part of a public API and another is an implementation detail, use an underscore as the prefix for the name of the private property:
Following the idea of this post:
class HomeListViewModel: ViewModel(){
val repositories: LiveData<List<Repo>> = MutableLiveData()
init {
repositories as MutableLiveData
...//Do some stuff to repositories
}
}
I haven't found any elegant solution to this problem however this is how I handle it.
private val selectedPositionLiveData = MutableLiveData<Int>()
fun getSelectedPosition() = selectedPositionLiveData as LiveData<Int>
The View observes via the public getter method and there's no need to define a second member in the ViewModel. I probably favour this approach due to my Java background with explicit getters but this seems to me to be as clean and concise as any of the other workarounds.
val doesn't have a setter since it's readonly but if you want a var you can do this
var repositories = MutableLiveData<List<String>>()
private set
var repoLoadError = MutableLiveData<Boolean>()
private set
var loading = MutableLiveData<Boolean>()
private set
This will give you a private setter and a public getter

Categories

Resources