I'm creating the app uses API and I have function inside the ViewModel which needs name of the city to fetch data from web. But it does not work. I have already worked with two way data binding in Java Android, but in Kotlin case something is wrong. In the CityViewModel I have an ObservableField which is binded with an Edit Text. This is place for name of city, then after button click is launching val currentWeatherByCity, but in logs is response error from API. If I set just string to the currentWeatherByCity as for example "London" API works, but if I want to use ObservableField app crashes.
CityViewModel:
class CityViewModel(
private val weatherRepository: WeatherRepository
): ViewModel() {
val city = ObservableField<String>()
private val metric: String = "metric"
val currentWeatherByCity by lazyDeferred {
weatherRepository.getCurrentWeatherByCity(city.get().toString(), metric)
}
}
CityActivity:
private fun bindUI() = launch(Dispatchers.Main) {
val cityWeather = cityViewModel.currentWeatherByCity.await()
cityWeather.observe(this#CityActivity, Observer {
if (it == null) return#Observer
})
ActivityCity xml:
<EditText android:layout_width="match_parent"
android:layout_height="50dp"
app:layout_constraintTop_toTopOf="parent"
android:gravity="center"
android:foregroundGravity="center_vertical"
android:textColor="#android:color/white"
android:textSize="20sp"
android:text="#={cityViewModel.city}"
android:hint="London/London,uk"
android:id="#+id/city_edit_text"
android:layout_marginEnd="50dp"
app:layout_constraintEnd_toStartOf="#+id/search_city_button"
android:inputType="text"
android:autofillHints="username"/>
I also checked the ObservableField in logs and city is null. If I set the String inside ObservableField then is right and API fetches data.
Related
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.
Overview:
Greetings. In my app the user can enter a number in an EditText and that number is then used as the maximum range for generating random numbers from 1 to (the number the user entered), which should then be displayed in a TextView.
Problem:
When the number is entered by the user and the button is pressed to generate the random number within the TextView, it doesn't work as intended.
For instance:
Initially, the value the LiveData holds for bonusNumber is 1 as it should be (based on the init block), but when the button is pressed to display a random number 0 is displayed after the button is pressed to generate the random number, instead of an actual random number between 1 and whatever the user entered in the EditText.
Code:
ViewModel:
class QuickPickViewModel : ViewModel() {
private val repository = Repository()
// LiveData for the number entered by the user
private var _userBonusNumber = MutableLiveData<Int>()
val userBonusNumber: LiveData<Int>
get() = _userBonusNumber
// LiveData for the actual result of the randomly generated bonus number
private var _bonusNumber = MutableLiveData<Int>()
val bonusNumber: LiveData<Int>
get() = _bonusNumber
init {
_userBonusNumber.value = 1
_bonusNumber.value = 1
}
fun getRandomBonusNumber() {
// Set the MutableLiveData to be displayed in the TextView, to the number the user entered
_bonusNumber.value = repository.generateBonusRandomNumber(_userBonusNumber.value!!.toInt())
}
}
Fragment:
class QuickPickFragment : Fragment() {
private lateinit var binding: FragmentQuickPickBinding
private lateinit var viewModel: QuickPickViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment using data binding
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_quick_pick,
container,
false
)
viewModel = ViewModelProvider(this).get(QuickPickViewModel::class.java)
binding.quickPickViewModel = viewModel
binding.lifecycleOwner = this
return binding.root
}
}
XML Views(The Relevant Ones):
<TextView
android:id="#+id/bonus_result_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="#{String.valueOf(quickPickViewModel.bonusNumber)}"
android:textAllCaps="true"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/main_result_text_view" />
<EditText
android:id="#+id/bonus_set_edit_text"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:ems="10"
android:hint="#string/enter_number_quick_pick_bonus_edit_text_hint"
android:importantForAutofill="no"
android:inputType="number"
android:text="#{String.valueOf(quickPickViewModel.userBonusNumber)}"
app:layout_constraintBaseline_toBaselineOf="#+id/bonus_set_title_text_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/bonus_set_title_text_view" />
<Button
android:id="#+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:onClick="#{() -> quickPickViewModel.getRandomBonusNumber()}"
android:text="Generate"
android:textAllCaps="true"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
Repository:
class Repository {
/**
* Generate random number for the bonus number within a particular range set by user
*
* [maxNum]: the number that the user entered which acts as the max range for the random numbers
*/
fun generateBonusRandomNumber(maxNum: Int): Int {
val minNum = 1
// Adding 1[minNum] to the bound makes sure the number the person entered is inclusive
return Random().nextInt(((maxNum - minNum)) + minNum)
}
}
Solution:
As suggested by a comment from #AppDev I could use two-way data-binding.
My previous code:
android:inputType="number"
android:text="#{String.valueOf(quickPickViewModel.userBonusNumber)}"
Should be changed to:
android:inputType="number"
android:text="#={quickPickViewModel.userBonusNumber}"
Then my ViewModel should be updated as follows:
class QuickPickViewModel : ViewModel() {
private val repository = Repository()
val userBonusNumber = MutableLiveData<String>()
private var _bonusNumber = MutableLiveData<Int>()
val bonusNumber: LiveData<Int>
get() = _bonusNumber
init {
userBonusNumber.value = "1"
_bonusNumber.value = 1
}
fun getRandomBonusNumber() {
_bonusNumber.value =
repository.generateBonusRandomNumber(userBonusNumber.value!!.toInt())
}
}
Note:
As stated in the comments, two way data-binding isn't always safe because it exposes the actual MutableLiveData which has write capabilities, while LiveData is read-only and safer, but in my case it wasn't applicable because that data, as stated before is read only and the data entered by the user wouldn't change. Thanks to the comments I was aware of my mistake.
So it would be safer to just capture the value from the EditText using the binding object within the Fragment and then setting the TextView to the value. However for my solution above, I just went with the two way data-binding just to show it in effect since there aren't many examples online for Kotlin.
Just thought for future readers, I would leave all of this information. Take care.
Android data binding does not observe kotlin's liveData builder
the following code will create a LiveData and it's supposed to be observed by data binding in XML but it doesn't work
val text =
liveData(Dispatchers.Default) {
emit("Hello")
}
on the other hand if it's gets observed in Kotlin it works fine
vm.text.observe(lifeCycleOwner,{
binding.texti.text = it
})
the xml:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/texti"
android:text="#{viewModel.text}"
/>
and if i change the definition of live data to:
val text = MutableLiveData("Hello")
data binding automatically observes and sets the text
Am i doing it wrong or it's a bug?
Did you set lifecycle owner of the binding? The binding initialization should look like:
override fun onCreate(savedInstanceState: Bundle?) {
val binding = DataBindingUtil.setContentView<MyActivityBinding>(this, R.layout.my_activity)
binding.setLifecycleOwner(this)
binding.viewModel = viewModel.get()
...
}
Fore more details, see: Use LiveData to notify the UI about data changes
I want to update views via databinding with livedata. Lets have a look at the scenario.
Data Class
data class Movie(var name: String = "", var createdAt: String = "")
ViewModel
class MyViewModel: ViewModel(){
var pageTitle: MutableLiveData<String>()
var movie: MutableLiveData<Movie>()
fun changeTitleAndMovieName()
pageTitle.value = "Title Changed"
movie.value.name = "Movie Name Changed"
}
}
XML
<layout>
...
<TextView
...
android:text="#{`Title: `+viewmodel.pageTitle `Name: `+viewmodel.movie.name}"/>
<Button
...
android:onClick="#{() -> viewmodel.changeTitleAndMovieName()}"/>
</layout>
What I want to do?
When the button is pressed, the title and the name of movie should change and reflect to the view.
What is happening now?
Only page title is changing because of String type LiveData.
Movie name is NOT being reflected in the view because of Movie type LiveData and I am changing the property of Movie type LiveData's property.
Is there any way to update Movie type LiveData to the view when any property is changed of the Movie.
I dont want to re-assign the object to the livedata e.g. viewmodel.movie.value = Movie(...)
I have got the answer of my question. A hint from Here
The reference link's answer is a bit long change todo. I have got a very simple solution.
Here's what I did:
Just inherit you Data Class with BaseObservable and just call the method notifyChange() after your Object's property change from anywhere.
i.e.
Data Class
data class Movie(var name: String = "", var createdAt: String = "") : BaseObservable()
ViewModel
class MyViewModel: ViewModel(){
var pageTitle: MutableLiveData<String>()
var movie: MutableLiveData<Movie>()
fun changeTitleAndMovieName()
pageTitle.value = "Title Changed"
movie.value.name = "Movie Name Changed"
//here is the megic
movie.value.notifyChange()
}
}