Why does my data-binding with EditText not work? - android

I have this class which I use for Room:
data class Piece(
#ColumnInfo(name = "title")
var title: String
)
Then I have a form class, which simply holds string values which I want to be shown in EditText.
class CreateEditPieceForm {
var title: String = ""
}
My ViewModel holds instances of these classes:
class EditPieceViewModel(...) : AndroidViewModel(application) {
val piece : LiveData<Piece?> = database.getMyPiece() // valid Piece with title set
val form = CreateEditPieceForm()
}
In my fragment I observe the piece:
viewModel.piece.observe(this, Observer {piece ->
piece?.let {
viewModel.updateInputValues(piece)
}
})
updateInputValues function in the ViewModel simply sets values in the form:
fun updateInputValues(piece: Piece) {
Log.d("mylog", "value: " + piece.title) // logs correct value
form.title = piece.title // setting this does not change EditText
}
And finally, in my layout, I try to use data binding to show the text from form.title in EditText:
<data>
<variable
name="viewModel"
type="com.example.tutorial.createedit.CreateEditPieceViewModel" />
</data>
<!-- ... -->
<EditText
android:id="#+id/input_title"
<!-- ... -->
android:inputType="text"
android:text="#={viewModel.form.title}" />
When I open the Screen with this fragment, EditText is empty. I know that the query for piece title is correct, because I log it before I set EditText's text attribute.
When I type something in the empty field, value of viewModel.form.title is being set with that value.
Why does it not set right at the beginning?

Databinding is not to be confused with
View binding.
Like in the guide, make EditPieceViewModel implement Observable and make form.title #Bindable.

Now it works. I still do not fully understand the Databinding library, so any advice to improve the code is welcomed!
I changed my ViewModel to implement Observable and added some methods to it:
// Make sure to import the correct Observable interface
import androidx.databinding.Observable
// ...
class EditPieceViewModel(...) : AndroidViewModel(application), Observable {
// ...
// New getter and setter methods for my title field:
#Bindable
fun getTitle() : String {
return form.title
}
fun setTitle(value: String) {
if(form.title != value) {
form.title = value
notifyPropertyChanged(BR.title) // This line is important for EditText' value to update
}
}
// New methods added for Observable:
override fun addOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.add(callback)
}
override fun removeOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.remove(callback)
}
/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the #Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* #param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks.notifyCallbacks(this, fieldId, null)
}
}
In layout xml I changed title to observe viewModel.title instead of viewModel.form.title:
<EditText
// ...
android:text="#={viewModel.title}" />
Finally in updateInputValues I call my custom setter:
fun updateInputValues(piece: Piece) {
setTitle(piece.title)
}

Related

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
}
}

Jetpack Compose: Modify Room data class using TextField

Modifying simple values and data classes using EditText is fairly straight forward, and generally looks like this:
data class Person(var firstName: String, var lastName: Int)
// ...
val (person, setPerson) = remember { mutableStateOf(Person()) }
// common `onChange` function handles both class properties, ensuring maximum code re-use
fun <T> onChange(field: KMutableProperty1<Person, T>, value: T) {
val nextPerson = person.copy()
field.set(nextPerson, value)
setPerson(nextPerson)
}
// text field for first name
TextField(
value = person.firstName,
onChange = { it -> onChange(Person::firstName, it) })
// text field for last name name
TextField(
value = person.lastName,
onChange = { it -> onChange(Person::lastName, it) })
As you can see, the code in this example is highly reusable: thanks to Kotlin's reflection features, we can use a single onChange function to modify every property in this class.
However, a problem arises when the Person class is not instantiated from scratch, but rather pulled from disk via Room. For example, a PersonDao might contain a `findOne() function like so:
#Query("SELECT * FROM peopleTable WHERE id=:personId LIMIT 1")
fun findOne(personId: String): LiveData<Person>
However, you cannot really use this LiveData in a remember {} for many reasons:
While LiveData has a function called observeAsState(), it returns State<T> and not MutableState<T>, meaning that you cannot modify it with the TextFields. As such this does not work:
remember { personFromDb.observeAsState()}
You cannot .copy() the Person that you get from your database because your component will render before the Room query is returned, meaning that you cannot do this, because the Person class instance will be remembered as null:
remember { mutableStateOf(findPersonQueryResult.value) }
Given that, what is the proper way to handle this? Should the component that contains the TextFields be wrapped in another component that handles the Room query, and only displays the form when the query is returned? What would that look like with this case of LiveData<Person>?
I would do it with a copy and an immutable data class
typealias PersonID = Long?
#Entity
data class Person(val firstName: String, val lastName: String) {
#PrimaryKey(autoGenerate = true)
val personID: PersonID = null
}
//VM or sth
object VM {
val liveData: LiveData<Person> = MutableLiveData() // your db call
val personDao: PersonDao? = null // Pretending it exists
}
#Dao
abstract class PersonDao {
abstract fun upsert(person: Person)
}
#Composable
fun test() {
val personState = VM.liveData.observeAsState(Person("", ""))
TextField(
value = personState.value.firstName,
onValueChange = { fName -> VM.personDao?.upsert(personState.value.copy(firstName = fName))}
)
}

Android - LiveData doesn't get updated

In my fragment I observe dbQuestionsList field:
viewModel.dbQuestionsList.observe(viewLifecycleOwner, Observer { list ->
Log.i("a", "dbQuestionsList inside fragment = $list ")
})
In my fragment I have few buttons and depending on which one is pressed I call method on viewModel passing the string which was set as tag to the button.
viewModel.onFilterChanged(button.tag as String)
My ViewMode:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("")
}
private fun onFilterChanged(filter: String) {
dbQuestionsList = mRepository.getDbQuestionsForCategory(filter)
}
Repository method:
fun getDbQuestionsForCategory(categoryName: String): LiveData<List<DatabaseQuestion>> {
return database.dbQuestionsDao().getDbQuestionsByCategory(categoryName)
}
Dao method:
#Query("SELECT * FROM db_questions_database WHERE category = :categoryId")
fun getDbQuestionsByCategory(categoryId: String): LiveData<List<DatabaseQuestion>>
When I press button, viewModel method is called with argument which should be used to update LiveData by searching through room database, but NOTHING gets updated for no reason. Database is not empty so there is no reason to return null and not trigger observer in main Fragment.
But when I do this in my viewModel:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("travel")
}
where I hardcode parameter, the room will return list and observer in fragment will be triggered, so it works like that but doesn't work when arguments is passed when button is pressed, Please explain because this thing doesn't make sense. I tried with mutable live data, with using .setValue and .postValue but NOTHING works.
The reason you aren't getting updates is because onFilterChanged() is reassigning dbQuestionsList, not updating it. So the variable you observe initially is never actually modified.
I would probably implement this using a Transformation:
val filter = MutableLiveData<String>().apply { value = "" }
val dbQuestionsList = Transformations.switchMap(filter) {
mRepository.getDbQuestionsForCategory(filter)
}
Then in your fragment just set the filter when your button is clicked:
viewModel.filter.value = button.tag as String
Try this:
dbQuestionsList.value = mRepository.getDbQuestionsForCategory(filter)
or
dbQuestionsList.postValue(mRepository.getDbQuestionsForCategory(filter))

Databinding can't call static function with String.function()

I have a function which formats some text
fun String.formatTo(): String {
if (this.isNotEmpty()) {
val value = this.toDouble()
return "%.02f".format(value)
}
return ""
}
And I want to apply this fun to my textView, using databinding, so I called in textView android:text="#{viewModel.text.formatTo()}", importing class in data of my layout
<data>
<import type="com.project.utils.extensions.ExtKt"/>
<variable
name="viewModel"
type="com.project.ViewModel" />
</data>
But I've got an error throw building:
Found data binding errors.
****/ data binding error ****msg:cannot find method formatTo() in class java.lang.String
What is a problem?
Create an object named ExtKt (or anything you want) and define your extension function in it and annotate it with #JvmStatic like below
#JvmStatic
fun String.formatTo(): String {
if (this.isNotEmpty()) {
val value = this.toDouble()
return "%.02f".format(value)
}
return ""
}
Update
android:text="#{ExtKt.formatTo()}"
Databinding is still Java modules, so some features of kotlin like extension functions can't be used there. The only thing you can do here - create specific function in your ViewModel class.
class ViewModel {
val text: String
...
fun getDisplayText(): String = text.formatTo()
}
May be you want to use calculated properties.
val displayText: String get() = text.formatTo()
Anyway, your xml call will look like following:
android:text="#{viewModel.displayText}"
Consider use MediatorLiveData:
class ViewModel(
val list: MutableLiveData<List<String>> = MutableLiveData<List<String>>()
) {
val listStr = MediatorLiveData<String>()
init {
listStr .addSource(list, Observer {
listStr .postValue(ViewModel.joinList(it))
})
}
companion object {
#JvmStatic fun joinList(list: List<String>): String {
return list.joinToString(separator = ", ")
}
}
}
And than in the xml:
<TextView
android:id="#+id/items"
android:text="#{viewModel.listStr}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

Kotlin: enums with associated values; function inside 'enum entry' gets `unresolved reference` error

I need to create a list of events with a string as name and a list of pairs as properties, some events properties are the static value, some need dynamically changed, so I create specific functions inside the enum entry to update it but complied with error unresolved reference:
Actually, what I want to implement is a list of enums with associated values,
something like these articles mentioned:
KT-4075 Allow setters overloading for properties, or
Kotlin: single property with multiple setters of different types, or
Using Kotlin’s sealed class to approximate Swift’s enum with associated data
Because I have more than 100 events, 95% of them are static, only several of them need to be updated during runtime, so sealed class might not suit my situation:
enum class Event(val eventName: String, vararg eventProperties: Pair<String, String?>) {
LOGIN_CLICKED("Login", ("View" to "button clicked")),
LOGIN_SUCCEED("Login", ("Type" to "succeed")),
LOGIN_ERROR("Login") {
fun errorMessage(errorMessage: String) {
eventProperties = listOf("ErrorType" to errorMessage)
}
},
// ... some other events
LIST_ITEM_CLICKED("LIST") {
fun listItemName(itemName: String) {
eventProperties = listOf("View" to itemName)
}
};
var eventProperties: List<Pair<String, String?>>? = listOf(*eventProperties)
// Although this approach can fix my problem, but I don't prefer it,
// because these functions are only meaningful to specific enum item,
// I don't want them be opened to all enum items.
//
// fun errorMessage(errorMessage: String) {
// eventProperties = listOf("ErrorType" to errorMessage)
// }
// fun listItemName(itemName: String) {
// eventProperties = listOf("View" to itemName)
// }
}
fun main(args: Array<String>) {
// unresolved reference
println(Event.LOGIN_ERROR.eventProperties)
Event.LOGIN_ERROR.errorMessage("error password")
println(Event.LOGIN_ERROR.eventProperties)
}
Because I have more than 100 events, 95% of them are static, only several of them need to be updated during runtime, so sealed class might not suit my situation
Why wouldn't it? If you are bothered with slightly longer declarations:
object LoginClicked : Event("Login", mapOf("View" to "button clicked"))
\\ vs
LOGIN_CLICKED("Login", mapOf("View" to "button clicked"))
you can create a helper enum class for them:
sealed class Event(val eventName: String, val eventProperties: Map<String, String?>) {
enum class Basic(val eventName: String, val eventProperties: Map<String, String?>) {
LOGIN_CLICKED("Login", mapOf("View" to "button clicked")),
LOGIN_SUCCEED("Login", mapOf("Type" to "succeed")),
...
}
class BasicEvent(b: Basic) : Event(b.eventName, b.eventProperties)
class LoginError(errorMessage: String) : Event("Login", mapOf("ErrorType" to errorMessage))
...
}

Categories

Resources