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

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

Related

Can't change the value of MutableList in a Kotlin class

I want to add the random generated integer into my MutableList in Player class when I use the random integer generator method located in Player class in my fragment then I want to pass this MutableList to Fragment with using Livedata(I'm not sure if i'm doing right with using livedata).Then show the MutableList in TextView.But MutableList returns the default value not after adding.
So what am i doing wrong ? What should i do ?
Thank you
MY CLASS
open class Player {
//property
private var playerName : String
private var playerHealth : Int
var playerIsDead : Boolean = false
//constructor
constructor(playerName:String,playerHealth:Int){
this.playerName = playerName
this.playerHealth = playerHealth
}
var numberss: MutableList<Int> = mutableListOf()
fun attack(){
//Create a random number between 1 and 10
var damage = (1..10).random()
//Subtract health points from the opponent
Log.d("TAG-DAMAGE-WRITE","$damage")
numberss.add(damage)
Log.d("TAG-DAMAGE-ADD-TO-LIST","$numberss")
Log.d("TAG-NUMBER-LIST-LATEST-VERSION","$numberss")
}
}
MY FRAGMENT
class ScreenFragment : Fragment() {
var nickname : String? = null
private lateinit var viewModel : DenemeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_screen, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(DenemeViewModel::class.java)
arguments?.let {
nickname = ScreenFragmentArgs.fromBundle(it).nickName
}
sFtextView.text = "Player : $nickname"
action()
}
private fun action(){
val health1 = (1..50).random()
val health2 = (1..50).random()
val superMan = Superman("Dusman",health1,)
while(superMan.playerIsDead == false){
//attack each other
superMan.attack()
sFtextsonuc.text = "Superman oldu"
viewModel.setData()
observeLiveData()
superMan.playerIsDead = true
}
}
fun observeLiveData(){
viewModel.damageList.observe(viewLifecycleOwner, Observer { dmgList ->
dmgList?.let {
sFtextsonuc.text = it.toString()
Log.d("TAG-THE-LIST-WE'VE-SENT-TO-FRAGMENT","$it")
}
})
}
}
MY VIEWMODEL
class DenemeViewModel : ViewModel() {
val damageList:MutableLiveData<MutableList<Int>> = MutableLiveData()
fun setData(){
damageList.value = Superman("",2).numberss
Log.d("TAG-VIEWMODEL","${damageList.value}")
}
}
MY LOG
PHOTO OF THE LOGS
Your Superman is evidently part of some ongoing game, not something that should be created and then destroyed inside the action function. So you need to store it in a property. This kind of state is usually stored in a ViewModel on Android so it can outlive the Fragment.
Currently, you are creating a Superman in your action function, but anything created in a function is automatically sent do the garbage collector (destroyed) if you don't store the reference in a property outside the function.
Every time you call the Superman constructor, such as in your line damageList.value = Superman("",2).numberss, you are creating a new instance of a Superman that has no relation to the one you were working with in the action() function.
Also, I recommend that you do not use LiveData until you fully grasp the basics of OOP: what object references are, how to pass them around and store them, and when they get sent to the garbage collector.
So, I would change your ViewModel to this. Notice we create a property and initialize it with a new Superman. Now this Superman instance will exist for as long as the ViewModel does, instead of only inside some function.
class DenemeViewModel : ViewModel() {
val superman = Superman("Dusman", (1..50).random())
}
Then in your frgament, you can get this same Superman instance anywhere you need to use it, whether it be to deal some damage to it or get the current value of its numberss to put in an EditText.
Also, I noticed in your action function that you have a while loop that repeatedly deals damage until Superman is dead. (It also incorrectly observes live data over and over in the loop, but we'll ignore that.) The problem with this is that the while loop is processed to completion immediately, so you won't ever see any of the intermediate text. You will only immediately see the final text. You probably want to put some delays inside the loop to sort of animate the series of events that are happening. You can only delay easily inside a coroutine, so you'll need to wrap the while loop in a coroutine launch block. In a fragment when working with views, you should do this with viewLifecycleOwner.lifecycleScope.launch.
Finally, if you set playerIsDead to true on the first iteration of the loop, you might as well remove the whole loop. I'm guessing you want to wrap an if-condition around that line of code. But since your code above doesn't modify player health yet, there's no sensible way to determine when a player should be dead, so I've commented out the while loop
private fun action() = viewLifecycleOwner.lifecycleScope.launch {
val superMan = viewModel.superman
//while(superMan.playerIsDead == false){
//attack each other
superMan.attack()
sFtextsonuc.text = "Superman oldu"
delay(300) // give user a chance to read the text before changing it
sFtextsonuc.text = superMan.numberss.joinToString()
delay(300) // give user a chance to read the text before changing it
// TODO superMan.playerIsDead = true
//}
}

Two-way databinding not working when triggered inside coroutine

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.

ViewSwitcher not switching view with .showNext() at initialization, after Livedata change is observed

I have a ViewSwitcher inside a DialogFragment. The switcher holds 2 views, a "1. selection view" and a "2. selected view".
The initial view is the defaulted to "1. selection view". When fromType is non-zero from the observing event database, I want the Livedata observer to call dialogViewSwitcher.showNext(), but the showNext() function is not called on dialog's initialization.
DialogFragment, inside onCreateDialog:
viewModel.event.observe(this, Observer {
it?.let {
viewModel.setCurrentFromType(it.fromPlaceType)
}
})
viewModel.currentFromType.observe(this, Observer {
if (it == 0) {
viewModel.setCurrentFromStatus(false)
} else {
viewModel.setCurrentFromStatus(true)
}
})
viewModel.currentFromStatus.observe(this, Observer {
binding.dialogViewSwitcher.showNext()
})
ViewModel:
private val _currentFromType = MutableLiveData(0)
val currentFromType: LiveData<Int>
get() = _currentFromType
fun setCurrentFromType(fromType: Int) {
_currentFromType.value = fromType
}
private val _currentFromStatus = MutableLiveData(false)
val currentFromStatus: LiveData<Boolean>
get() = _currentFromStatus
fun setCurrentFromStatus(status: Boolean) {
_currentFromStatus.value = status
}
Log shows the currentFromStatus observed the change (from false to true) when the dialog opens, but I'm thinking that because the dialogViewSwitcher hasn't initialized yet inside onCreatDialog when the change to currentFromStatus was observed, so showNext() did nothing. I also looked into data binding and didn't find a ViewSwitcher property in xml that shows the second view. What should I do to fix this behaviour?
Didn't find a good way to programmatically showNext() at onCreate. Instead I deleted the viewswitcher and use databinding on the visibility of each of the 2 child views, and this way works well.

How to programically trigger notify on MutableLiveData change

I have a LiveData property for login form state like this
private val _authFormState = MutableLiveData<AuthFormState>(AuthFormState())
val authFormState: LiveData<AuthFormState>
get() =_authFormState
The AuthFormState data class has child data objects for each field
data class AuthFormState (
var email: FieldState = FieldState(),
var password: FieldState = FieldState()
)
and the FieldState class looks like so
data class FieldState(
var error: Int? = null,
var isValid: Boolean = false
)
When user types in some value into a field the respective FieldState object gets updated and assigned to the parent AuthFormState object
fun validateEmail(text: String) {
_authFormState.value!!.email = //validation result
}
The problem is that the authFormState observer is not notified in this case.
Is it possible to trigger the notification programically?
Maybe you can do:
fun validateEmail(text: String) {
val newO = _authFormState.value!!
newO.email = //validation result
_authFormState.setValue(newO)
}
You have to set the value to itself, like this: _authFormState.value = _authFormState.value to trigger the refresh. You could write an extension method to make this cleaner:
fun <T> MutableLiveData<T>.notifyValueModified() {
value = value
}
For such a simple data class, I would recommend immutability to avoid issues like this altogether (replaces all those vars with vals). Replace validateEmail() with something like this:
fun validateEmail(email: String) = //some modified version of email
When validating fields, you can construct a new data object and set it to the live data.
fun validateFields() = _authFormState.value?.let {
_authFormState.value = AuthFormState(
validateEmail(it.email),
validatePassword(it.password)
)
}

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

Categories

Resources