I am working on a simple Android App using Kotlin. I want to use databindings, but I cannot get them work with an EditText
This is my XML Layout
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="de.knerd.applicationmanager.models.AgencyModel"/>
<variable name="agency" type="AgencyModel"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:showIn="#layout/activity_add_agency"
tools:context="de.knerd.applicationmanager.activities.AddAgencyActivity"
android:orientation="vertical"
android:layout_margin="16dp">
<android.support.design.widget.TextInputLayout android:layout_height="match_parent"
android:layout_width="match_parent">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:ems="10"
android:id="#+id/name"
android:labelFor="#+id/name"
android:hint="#string/agency_name"
android:text="#={agency.name}"/>
</android.support.design.widget.TextInputLayout>
</RelativeLayout>
</layout>
This is my Model class
package de.knerd.applicationmanager.models
import android.databinding.BaseObservable
import android.databinding.Bindable
import com.j256.ormlite.dao.ForeignCollection
import com.j256.ormlite.field.DatabaseField
import com.j256.ormlite.field.ForeignCollectionField
import com.j256.ormlite.table.DatabaseTable
import de.knerd.applicationmanager.BR
#DatabaseTable(tableName = "agency")
class AgencyModel : BaseObservable() {
#DatabaseField(unique = true, canBeNull = false)
#get:Bindable
#set:Bindable
var name: String? = null
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
#DatabaseField(generatedId = true)
#get:Bindable
var id: Int = 0
#ForeignCollectionField(eager = false)
#get:Bindable
var agents: ForeignCollection<AgentModel>? = null
}
And this is the part that binds the model to the view
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ContentAddAgencyBinding>(this, R.layout.content_add_agency)
binding.agency = AgencyModel()
setContentView(R.layout.activity_add_agency)
val toolbar = findViewById(R.id.toolbar) as Toolbar
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
}
When I try to access the properties of model that is in the binding I get a null value. The Code I use is this
private fun save(): Boolean {
try {
val agency = binding.agency
Log.d("Name", agency.name)
return true
} catch (ex: Exception) {
return false
}
}
I found the Problem, it was caused by me binding to the wrong layout. The correct Code is as follows.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityAddAgencyBinding>(this, R.layout.activity_add_agency)
binding.agency = AgencyModel()
val toolbar = findViewById(R.id.toolbar) as Toolbar
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
}
In both cases I had to bind to the activity layout
You are not using Binding the way it was meant to be Knerd. Two-Way binding is intended so that you can access objects or fields from code without ever having to reference the UI code. By you doing the binding.agency you are no different then findViewById() and you are defeating the purpose of binding.
Fix your binding problems first. Use BaseObservable for your object model and it should stay up to date with whatever values change on the screen.
Then when you save it should be nothing more then accessing the local variable.
private fun save(): Boolean {
try {
Log.d("Name", localObservableAgency.name)
return true
} catch (ex: Exception) {
return false
}
}
//not sure what this method is for, guessing you were try catching null. Better way would be to just do a null check on the object as catching exception by accessing null value is heavy and doesn't add value. Also Kotlin is null safe language, so you are either losing your value on the binding maybe by setting it to null to begin with or something is coded weirdly here as you have to intentionally make an object nullable if you keep a local copy. However, you weren't keeping a local copy you were just newing one up inline and accessing via binding later as if it was your repository of variables haha.
So long story short, is use data binding the way it was intended to be used and you won't have a problem. That is probably why someone down voted you is my guess.
Related
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)
I'm having some issue that two way binding with an Integer data type.
ViewModel
var saleType: MutableLiveData<Int> = MutableLiveData(1)
var saleDetail: MutableLiveData<Int> = MutableLiveData(0)
var salePrice: MutableLiveData<Int> = MutableLiveData(0)
var saleRate: MutableLiveData<Int> = MutableLiveData(0)
var printAmount: MutableLiveData<Int> = MutableLiveData(1)
layout.xml
<data>
<import type="com.package.utils.DataBindingConverters" />
<variable
name="viewModel"
type="com.package.ViewModel" />
</data>
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/sale_detail_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:gravity="right"
android:selectAllOnFocus="true"
android:inputType="number"
android:imeOptions="actionDone"
android:singleLine="true"
android:maxLines="1"
app:inputContent="#={DataBindingConverters.convertIntegerToString(viewModel.saleDetail)}"
/>
DataBindingConverters
class DataBindingConverters {
companion object {
#InverseMethod("convertStringToInteger")
#JvmStatic
fun convertStringToInteger(value: String): Int? {
if (TextUtils.isEmpty(value) || !TextUtils.isDigitsOnly(value)) {
return null
}
return value.toIntOrNull()
}
#InverseMethod("convertIntegerToString")
#JvmStatic
fun convertIntegerToString(value: Int?): String {
return value?.toString() ?: ""
}
}
}
It occurs error like this
cannot find method convertIntegerToString(java.lang.String) in class com.package.utils.DataBindingConverters
I thought it would be good to process the value for IntegerToString in two-way binding, but I get an error that can't find a method.
In two-way binding, is there any other way to handle Integer?
I don't know what app:inputContent does, but using android:text on the TextInputEditText should be enough:
<com.google.android.material.textfield.TextInputEditText
...
android:text="#={ viewModel.salePriceString }"/>
Now you wonder what is salePriceString. It should be your 2-way binding liveData, and because android:text requires String, salePriceString typing would be LiveData<String>. Modify your viewModel to:
val salePriceString = MutableLiveData<String?>(0.toString())
Now every time you edit the text (by user input) in the TextInputEditText, your salePricesString will get updated with the new text. And anytime you update your salePriceString through code, the text of your TextInputEditText will update accordingly. For the latter case, if you want to update your salePriceString with a value of 500 for example, you would do salePriceString.postValue(500.toString()). And if you want to get the Int value from salePriceString in your code, just do salePriceString.value?.toIntOrNull().
The next steps are optional, but if you want a convenient way to get the Int value without using salePriceString.value?.toIntOrNull() every time, modify your viewModel a bit further:
import androidx.lifecycle.Transformations
...
val salePriceString = MutableLiveData<String?>(0.toString())
// add conversion to LiveData<Int>
val salePrice = Transformations.map(salePriceString) { priceString ->
priceString?.toIntOrNull()
}
salePrice now contains the Int value of salePriceString, and is automatically updated every time salePriceString changes. salePrice is a LiveData, so you can observe it the same way you would observe salePriceString, but instead of salePriceString, you have the Int value ready in salePrice.
A final step is to observe salePrice somewhere in the code, most logically on the fragment that your viewModel is attached to:
viewModel.salePrice.observe(this, {}) // this, or viewLifecycleOwner, depending on your use case
You need to observe it, even if you then do nothing with the value. It is because Transformation.map is a bit strange one, the LiveData created by it won't go "live" unless there exists an observer for it. That means that until salePrice is observed by some observer, you won't get any value from it, i.e. salePrice.value would always be null.
I'm trying to update TextInputEditText text via data-binding after I get some data from BE API call. My solution works perfectly if code is not executed inside coroutine. If variable is set inside coroutine EditText does not get updated.
My XML code:
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="40dp"
android:text="#={ viewModel.name }" />
My viewModel code:
var name: String = ""
get() = field.trim()
set(value) {
field = value
//some other unrelated code
}
...
fun getName(){
name = "first"
viewModelScope.launch(Dispatchers.Main) {
name = "second"
}
}
TextInputEditText will be updated to "first" but not to "second". I've tried with other dispatchers. I've also verified via debugger that "name" variable setter is being triggered both times. It's just not updating the EditText. Any ideas on what could cause this?
In my case, the problem was solved by setting the value of the lifecycleOwner property in the following code. The data binding is now done as intended.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postDetailViewModel = ViewModelProvider(this)[PostDetailViewModel::class.java]
binding.varPostDetailViewModel = postDetailViewModel
binding.lifecycleOwner = this // Add this line
coroutineScope.launch {
arguments?.let {
val args = PostDetailFragmentArgs.fromBundle(it)
postDetailViewModel.getPostDetail(args.postID)
}
}
}
Your name field needs to be observable.
Right now, nothing is telling the EditText that the field was updated and needs to be rebound. You're probably seeing "first" from initially setting the viewModel on the binding.
Review the documentation on obervability.
My answer to another similar question might also be helpful.
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
}
}
So I have a viewHolder with a checkbox
here is my viewModel
#Bindable
var itemIsSelected: Boolean = isSelected
set(value) {
if (field != value) {
field = value
notifyPropertyChanged(BR.itemIsSelected) // this doesn't work
notifyChange() // this one works
}
}
here is my viewHolder class
inner class SpecialityItemViewHolder(val binding: ItemSpecialityFilterBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(specialityItemViewModel: SpecialityItemViewModel) {
binding.viewModel = specialityItemViewModel
binding.executePendingBindings()
this.itemView.setOnClickListener {
binding.viewModel?.let {
it.itemIsSelected = !it.itemIsSelected // this doesn't trigger ui changes
}
}
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="packagename.ItemViewModel" />
</data>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="#dimen/vertical_margin_small"
android:paddingBottom="#dimen/vertical_margin_small"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<Checkbox
android:id="#+id/checkbox"
android:layout_width="25dp"
android:layout_height="25dp"
android:checked="#={viewModel.itemIsSelected}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>
</layout>
so what happens is that the setting is working properly as in that when i press the checkbox it sets the backing field to the corresponding value
but when i set the backing field (notice code in bind function) it doesn't trigger ui change I know that calling binding.executePendingBindings() would solve the problem but my understanding is that notifyPropertyChanged(BR.itemIsSelected) should not need executePendingBindings call, Actually if i call notifyChange instead everything works properly ( but I presume there is performance issue here as it notifies change for all properties instead )
Your ViewModel class have to extend BaseObservable class and using kotlin you have to use #get:Bindable annotation. If you don't want to use BaseObservable as a parent class then use ObservableField<Boolean>(). You find more information in https://developer.android.com/topic/libraries/data-binding/observability#kotlin
the view-model needs Kotlin annotations, else the annotation processor will ignore it:
class ViewModel : BaseObservable() {
#get:Bindable
var isSelected: Boolean
set(value) {
if (isSelected != value) {
isSelected = value
notifyPropertyChanged(BR.isSelected)
}
}
}
it.isSelected is easier to read than it.itemIsSelected.