What is the best approach to validate form data using ViewModel and Databinding?
I have a simple Sign-Up activity that links binding layout and ViewModel
class StartActivity : AppCompatActivity() {
private lateinit var binding: StartActivityBinding
private lateinit var viewModel: SignUpViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, SignUpViewModelFactory(AuthFirebase()))
.get(SignUpViewModel::class.java);
binding = DataBindingUtil.setContentView(this, R.layout.start_activity)
binding.viewModel = viewModel;
signUpButton.setOnClickListener {
}
}
}
ViewModel with 4 ObservableFields and signUp() method that should validate data before submitting data to the server.
class SignUpViewModel(val auth: Auth) : ViewModel() {
val name: MutableLiveData<String> = MutableLiveData()
val email: MutableLiveData<String> = MutableLiveData()
val password: MutableLiveData<String> = MutableLiveData()
val passwordConfirm: MutableLiveData<String> = MutableLiveData()
fun signUp() {
auth.createUser(email.value!!, password.value!!)
}
}
I guess we can add four boolean ObservableFields for each input, and in signUp() we can check inputs and change state of boolean ObservableField that will produce an appearing error on layout
val isNameError: ObservableField<Boolean> = ObservableField()
fun signUp() {
if (name.value == null || name.value!!.length < 2 ) {
isNameError.set(true)
}
auth.createUser(email.value!!, password.value!!)
}
But I am not sure that ViewModel should be responsible for validation and showing an error for a user and we will have boilerplate code
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.maximdrobonoh.fitnessx.SignUpViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/colorGreyDark"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="#+id/appTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="#string/app_title"
android:textColor="#color/colorWhite"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="#+id/screenTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/appTitle">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="#string/sign"
android:textColor="#color/colorWhite"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/up"
android:textColor="#color/colorWhite"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:id="#+id/form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/screenTitle">
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_name"
android:inputType="textPersonName"
android:text="#={viewModel.name}" />
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_email"
android:inputType="textEmailAddress"
android:text="#={viewModel.email}"
/>
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_password"
android:inputType="textPassword"
android:text="#={viewModel.password}" />
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_confirm_password"
android:inputType="textPassword"
android:text="#={viewModel.passwordConfirm}" />
<Button
android:id="#+id/signUpButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="#drawable/button_gradient"
android:text="#string/sign_up_next_btn"
android:textAllCaps="true"
android:textColor="#color/colorBlack" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
</layout>
There can be many ways to implement this. I am telling you two solutions, both works well, you can use which you find suitable for you.
I use extends BaseObservable because I find that easy than converting all fields to Observers. You can use ObservableFields too.
Solution 1 (Using custom BindingAdapter)
In xml
<variable
name="model"
type="sample.data.Model"/>
<EditText
passwordValidator="#{model.password}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={model.password}"/>
Model.java
public class Model extends BaseObservable {
private String password;
#Bindable
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
notifyPropertyChanged(BR.password);
}
}
DataBindingAdapter.java
public class DataBindingAdapter {
#BindingAdapter("passwordValidator")
public static void passwordValidator(EditText editText, String password) {
// ignore infinite loops
int minimumLength = 5;
if (TextUtils.isEmpty(password)) {
editText.setError(null);
return;
}
if (editText.getText().toString().length() < minimumLength) {
editText.setError("Password must be minimum " + minimumLength + " length");
} else editText.setError(null);
}
}
Solution 2 (Using custom afterTextChanged)
In xml
<variable
name="model"
type="com.innovanathinklabs.sample.data.Model"/>
<variable
name="handler"
type="sample.activities.MainActivityHandler"/>
<EditText
android:id="#+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:afterTextChanged="#{(edible)->handler.passwordValidator(edible)}"
android:text="#={model.password}"/>
MainActivityHandler.java
public class MainActivityHandler {
ActivityMainBinding binding;
public void setBinding(ActivityMainBinding binding) {
this.binding = binding;
}
public void passwordValidator(Editable editable) {
if (binding.etPassword == null) return;
int minimumLength = 5;
if (!TextUtils.isEmpty(editable.toString()) && editable.length() < minimumLength) {
binding.etPassword.setError("Password must be minimum " + minimumLength + " length");
} else {
binding.etPassword.setError(null);
}
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
ActivityMainBinding binding;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setModel(new Model());
MainActivityHandler handler = new MainActivityHandler();
handler.setBinding(binding);
binding.setHandler(handler);
}
}
Update
You can also replace
android:afterTextChanged="#{(edible)->handler.passwordValidator(edible)}"
with
android:afterTextChanged="#{handler::passwordValidator}"
Because parameter are same of android:afterTextChanged and passwordValidator.
This approach uses TextInputLayouts, a custom binding adapter, and creates an enum for form errors. The result I think reads nicely in the xml, and keeps all validation logic inside the ViewModel.
The ViewModel:
class SignUpViewModel() : ViewModel() {
val name: MutableLiveData<String> = MutableLiveData()
// the rest of your fields as normal
val formErrors = ObservableArrayList<FormErrors>()
fun isFormValid(): Boolean {
formErrors.clear()
if (name.value?.isNullOrEmpty()) {
formErrors.add(FormErrors.MISSING_NAME)
}
// all the other validation you require
return formErrors.isEmpty()
}
fun signUp() {
auth.createUser(email.value!!, password.value!!)
}
enum class FormErrors {
MISSING_NAME,
INVALID_EMAIL,
INVALID_PASSWORD,
PASSWORDS_NOT_MATCHING,
}
}
The BindingAdapter:
#BindingAdapter("app:errorText")
fun setErrorMessage(view: TextInputLayout, errorMessage: String) {
view.error = errorMessage
}
The XML:
<layout>
<data>
<import type="com.example.SignUpViewModel.FormErrors" />
<variable
name="viewModel"
type="com.example.SignUpViewModel" />
</data>
<!-- The rest of your layout file etc. -->
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/text_input_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorText='#{viewModel.formErrors.contains(FormErrors.MISSING_NAME) ? "Required" : ""}'>
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Name"
android:text="#={viewModel.name}"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Any other fields as above format -->
And then, the ViewModel can be called from activity/fragment as below:
class YourActivity: AppCompatActivity() {
val viewModel: SignUpViewModel
// rest of class
fun onFormSubmit() {
if (viewModel.isFormValid()) {
viewModel.signUp()
// the rest of your logic to proceed to next screen etc.
}
// no need for else block if form invalid, as ViewModel, Observables
// and databinding will take care of the UI
}
}
I've written a library for validating bindable fields of an Observable object.
Setup your Observable model:
class RegisterUser:BaseObservable(){
#Bindable
var name:String?=""
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
#Bindable
var email:String?=""
set(value) {
field = value
notifyPropertyChanged(BR.email)
}
}
Instantiate and add rules
class RegisterViewModel : ViewModel() {
var user:LiveData<RegisterUser> = MutableLiveData<RegisterUser>().also {
it.value = RegisterUser()
}
var validator = ObservableValidator(user.value!!, BR::class.java).apply {
addRule("name", ValidationFlags.FIELD_REQUIRED, "Enter your name")
addRule("email", ValidationFlags.FIELD_REQUIRED, "Enter your email")
addRule("email", ValidationFlags.FIELD_EMAIL, "Enter a valid email")
addRule("age", ValidationFlags.FIELD_REQUIRED, "Enter your age (Underage or too old?)")
addRule("age", ValidationFlags.FIELD_MIN, "You can't be underage!", limit = 18)
addRule("age", ValidationFlags.FIELD_MAX, "You sure you're still alive?", limit = 100)
addRule("password", ValidationFlags.FIELD_REQUIRED, "Enter your password")
addRule("passwordConfirmation", ValidationFlags.FIELD_REQUIRED, "Enter password confirmation")
addRule("passwordConfirmation", ValidationFlags.FIELD_MATCH, "Passwords don't match", "password")
}
}
And setup your xml file:
<com.google.android.material.textfield.TextInputLayout
style="#style/textFieldOutlined"
error='#{viewModel.validator.getValidation("email")}'
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/email"
style="#style/myEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Your email"
android:imeOptions="actionNext"
android:inputType="textEmailAddress"
android:text="#={viewModel.user.email}" />
What you have in mind is right, actually. The viewmodel should not know anything about the android system and will only work with pure java/kotlin. Thus, doing what you are thinking of is right. ViewModel's shouldn't know about the android system as all view interactions should be handled on the View. But, their properties can be bounded to the view.
TL;DR
This will work
fun signUp() {
if (name.value == null || name.value!!.length < 2 ) {
isNameError.set(true)
}
auth.createUser(email.value!!, password.value!!)
}
Suggestion
I would suggest, if you would like to dig in deeper, you could use Custom Binding Adapters. This way you:
won't need additional variables to your view model
have a cleaner view model since the error handling is on the custom binding adapter
would easier read on your XML views as you could define there the validations you need
I'll let your imagination fly on how you could make the custom binding adapter only have the validations. For now, it's better to understand the basics of custom binding adapters.
Cheers
Yes, you can use your validation logic from ViewModel, because you're having your observable variables from ViewModel & your xml is also deriving data from ViewModel class also.
So, Solution :
You can create #BindingAdapter in ViewModel and bind your
button click with it. Check your validation there and do some other
stuffs also.
You can create Listener, and implement it on ViewModel to receive clicks from button and bind that listener to xml.
You can use Two-Way data binding also (Be aware of infinite loops though).
//Let's say it's your binding adapter from ViewModel
fun signUp() {
if (check validation logic) {
// Produce errors
}
// Further successful stuffs
}
Related
I have to implement click listener using binding and ViewModel as per clean architecture.
I have two button to select language like English and Chinese.
LanguageActivity.kt
#AndroidEntryPoint
class LanguageActivity : PBActivity(R.layout.activity_language) {
private val mViewModel:LanguageViewModel by viewModels()
private val mBinding:ActivityLanguageBinding by viewbind()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding.apply {
lifecycleOwner = this#LanguageActivity
viewModel = mViewModel
}
collectFlow(mViewModel.uiState){
Toast.makeText(this, it.name, Toast.LENGTH_SHORT).show()
when(it){
LanguageSelected.ENGLISH -> {
}
LanguageSelected.CHINESE -> {
}
LanguageSelected.NONE -> {
}
}
}
}
}
enum class LanguageSelected{
ENGLISH,CHINESE,NONE
}
LanguageViewModel.kt
#HiltViewModel
class LanguageViewModel #Inject constructor(
private val pbPrefs: PBPrefs
): ViewModel(){
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LanguageSelected.NONE)
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LanguageSelected> = _uiState
fun englishSelected()= viewModelScope.launch {
_uiState.value = LanguageSelected.ENGLISH
pbPrefs.setLanguageSelect()
}
fun urduSelected() = viewModelScope.launch {
_uiState.value = LanguageSelected.URDU
pbPrefs.setLanguageSelect()
}
}
activity_language.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="uk.co.planetbeyond.telenorbluecollar.ui.language.LanguageViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.language.LanguageActivity">
<androidx.constraintlayout.widget.Guideline
android:id="#+id/center_vertical_gl"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<Button
android:id="#+id/englishTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="English "
android:onClick="#{() -> viewModel.englishSelected()}"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="#id/center_vertical_gl"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/urduTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Urdu"
android:onClick="#{() -> viewModel.urduSelected()}"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="#id/center_vertical_gl"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I have to select language on click of a button. What's the best approach as per clean architecture to implement
I have created an interface LanguageSelected but for that I have to created multiple methods in viewModel, As languages increase I have to create more methods in viewmodel.
How could I make it short or I mean extensible?
I have a ViewModel class defined as follows:
class StockLoadTaskModel : ViewModel() {
....
....
var d: Double = 10.0
}
That is bound to the following 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">
<data>
<import type="android.view.View" />
<import type="it.kfi.lorikeetmobile.extras.Converter" alias="Converter"/
<variable
name="viewModel"
type="it.kfi.lorikeetmobile.stock.models.StockLoadTaskModel" />
<variable
name="view"
type="it.kfi.lorikeetmobile.stock.ui.movements.StockLoadTaskFragment
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
...
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/hint_et_item_code"
android:text="#={viewModel.itemCode}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_quantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={Converter.doubleToString(d)}"
android:hint="#string/quantity" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_note"
android:lines="3"
android:scrollbars="vertical"
android:overScrollMode="ifContentScrolls"
android:gravity="top"
android:inputType="textMultiLine"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/hint_et_note"
android:text="#={viewModel.selectedItem.detail.note}"/>
</com.google.android.material.textfield.TextInputLayout>
...
</LinearLayout>
And I have also the following Converter object:
object Converter {
#JvmStatic
#InverseMethod("stringToDouble")
fun doubleToString(value: Double?): String? {
if (value == null) {
return null
}
return DecimalFormat(ClientConfiguration.currentConfig.decimalFormat).format(value)
}
#JvmStatic
fun stringToDouble(value: String?): Double? {
if (value == null) {
return null
}
val v = DecimalFormat(ClientConfiguration.currentConfig.decimalFormat).parse(value)
return v.toDouble()
}
}
If I set: android:text="#={Converter.doubleToString(d)}" (two-way databinding), in the EditText with id et_quantity I get the following error:
...error: cannot find symbol
If I change it into a one-way databinding like: android:text="#{Converter.doubleToString(d)}", it works. It looks like the binding manager is not able to recognize the inverse method.
Can anybody help me? Thank you.
Why the error happens?
When you define two-way data binding like you have in your example android:text="#={Converter.doubleToString(d)}" the question is: what function/object will receive data that you get back passed from EditText as user types data in? Should data be passed to Converter.doubleToString or maybe some other static function of Converter? Maybe to the result of Converter.doubleToString(d) or to d variable?
You must be precise.
You expect it is d, the compiler expects it is the result of Converter.doubleToString(d). Actually, neither will work.
Another issue is that EditText does operate with characters. It knows nothing about double, int, float, byte, short, boolean or anything else that is not a string.
It means that in order to implement two-way data binding your source:
must return value of type String;
must be assignable.
How to fix the issue?
Android architecture components introduce us with ObservableField class. There are ready to use ObservableBoolean, ObservableChar, ObservableFloat and a few others. If you open the link from the previous sentence you should see all of the classes Observable... on the left pane.
There is no ObservableString but ObservableField accepts a generic type. So you can define a variable that is a part of data binding to be ObservableField<String>("defaultValueHere").
So what you should have is:
class StockLoadTaskModel : ViewModel() {
....
....
var d: Double = 10.0
var dataBindingVariable = ObservableField<String>(d.toString())
}
The dataBindingVariable will always return you the contents of an EditText you bound it to. You can get that value and safely convert to double.
class StockLoadTaskModel : ViewModel() {
....
....
var d: Double = 10.0
var dataBindingVariable =
object: ObservableField<String>(d.toString()) {
override fun set(value: String?) {
super.set(value)
// a value has been set
d = value.toDoubleOrNull() ?: d
}
}
}
Layout declaration will look like that for input field:
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_quantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={viewModel.dataBindingVariable}"
android:hint="#string/quantity" />
</com.google.android.material.textfield.TextInputLayout>
And there will be no need for object Converter.
There is another way of doing two-way data binding I'm not talking about here because it was already answered. Here it is.
I am writing an app to display NHL scores, and would like for each team in the RecyclerView to have their logo next to it. There is a URL that I can request with a team's ID that will return a hi-res image of the team's logo. I am trying to make it so that I can load the images in my viewModel and set them in the view, as I'm doing for things like the team name, current score, etc.
I have tried using Picasso for this, but it requires a context, which the viewModel doesn't have, and the viewModel cannot directly access the imageView to be able to change it. So how can I load the images and expose them either with data binding or something else, to allow the view to display them?
Here is my MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: GameListViewModel
private var errorSnackbar: Snackbar? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
binding.gameList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
viewModel = ViewModelProviders.of(this).get(GameListViewModel::class.java)
viewModel.errorMessage.observe(this, Observer { errorMessage ->
if (errorMessage != null)
showError(errorMessage)
else
hideError()
})
binding.viewModel = viewModel
}
private fun showError(#StringRes errorMessage:Int) {
errorSnackbar = Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_INDEFINITE)
errorSnackbar?.setAction(R.string.retry, viewModel.errorClickListener)
errorSnackbar?.show()
}
private fun hideError() {
errorSnackbar?.dismiss()
}
}
ViewModel:
class GameViewModel:BaseViewModel() {
private val awayTeamName = MutableLiveData<String>()
private val homeTeamName = MutableLiveData<String>()
private val awayTeamScore = MutableLiveData<String>()
private val homeTeamScore = MutableLiveData<String>()
private val timeRemaining = MutableLiveData<String>()
fun bind(response: Game) {
awayTeamName.value = response.gameData.teams.away.name
homeTeamName.value = response.gameData.teams.home.name
awayTeamScore.value = response.liveData.linescore.teams["away"]?.goals.toString()
homeTeamScore.value = response.liveData.linescore.teams["home"]?.goals.toString()
if (response.gameData.status.detailedState == "Scheduled") {
val parser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault())
parser.timeZone = TimeZone.getTimeZone("UTC")
val formatter = SimpleDateFormat("hh:mm a", Locale.getDefault())
formatter.timeZone = TimeZone.getDefault()
timeRemaining.value = formatter.format(parser.parse(response.gameData.datetime.dateTime))
} else {
timeRemaining.value = response.liveData.linescore.currentPeriodTimeRemaining + " " + response.liveData.linescore.currentPeriodOrdinal
}
}
fun getAwayTeamName(): MutableLiveData<String> {
return awayTeamName
}
fun getHomeTeamName(): MutableLiveData<String> {
return homeTeamName
}
fun getAwayTeamScore(): MutableLiveData<String> {
return awayTeamScore
}
fun getHomeTeamScore(): MutableLiveData<String> {
return homeTeamScore
}
fun getTimeRemaining(): MutableLiveData<String> {
return timeRemaining
}
}
and XML for the recyclerView row:
<?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>
<variable
name="viewModel"
type="com.example.nhlstats.ui.game.GameViewModel" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="#+id/awayTeam"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="12dp">
<ImageView
android:id="#+id/awayTeamLogo"
android:layout_height="36dp"
android:layout_width="0dp"
android:layout_weight="1"
tools:src="#drawable/ic_launcher_background"/>
<TextView
android:id="#+id/awayTeamName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:layout_gravity="center_vertical"
android:text="#{viewModel.awayTeamName}"
tools:text="CHI Blackhawks"/>
<TextView
android:id="#+id/awayScore"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:text="#{viewModel.awayTeamScore}"
tools:text="0"/>
<TextView
android:id="#+id/gameTime"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:text="#{viewModel.timeRemaining}"
tools:text="14:26 3rd"/>
</LinearLayout>
<LinearLayout
android:id="#+id/homeTeam"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="24dp">
<ImageView
android:id="#+id/homeTeamLogo"
android:layout_height="36dp"
android:layout_width="0dp"
android:layout_weight="1"
tools:src="#drawable/ic_launcher_background"/>
<TextView
android:id="#+id/homeTeamName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:layout_gravity="center_vertical"
android:text="#{viewModel.homeTeamName}"
tools:text="CAR Hurricanes"/>
<TextView
android:id="#+id/homeScore"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="2"
android:text="#{viewModel.homeTeamScore}"
tools:text="4"/>
</LinearLayout>
</LinearLayout>
</layout>
Thanks in advance.
Using data binding you should create a custom binding adapter like below:
#BindingAdapter("app:imageUri")
fun loadImageWithUri(imageView: ImageView, imageUri: String){
Glide.with(imageView.context).load(Uri.parse(imageUri)).into(imageView)
}
And change you imageview like this:
<androidx.appcompat.widget.AppCompatImageView
android:layout_height="36dp"
android:layout_width="0dp"
android:layout_weight="1"
app:imageUri="#{viewmodel.teamLogoUri}"/>
For Android Architecture Components View Model,
It's not a good practice to pass your Activity Context to the Activity's ViewModel as its a memory leak. I don't support to to that.
You can create image url observer in viewmodel and observe it in your View class (Activity or fragment), Like this (as Duy Khanh Nguyen answered):-
viewModel.url.observe(this, Observer {
it?.let { url ->
//So image into itemView using Picasso
}
})
But if you want to go with otherwise you can simply use an Application context which is provided by the AndroidViewModel, you should extend AndroidViewModel which is simply a ViewModel that includes an Application reference. I your case do it into your BaseViewModel. Example:-
class BaseViewModel(application: Application) : AndroidViewModel(application) {
val context = getApplication<Application>().applicationContext
//... ViewModel methods
}
I guest you'll create GameViewModel for each itemView, so when binding the view holder:
Your GameViewModel class
val awayLogoUrl = MutableLiveData<String>()
val homeLogoUrl = MutableLiveData<String>()
fun bind(response: Game) {
awayLogoUrl.value = response... //set away logo url here
homeLogoUrl.value = response... //set home logo url here
}
Your ViewHolder class
viewModel.awayLogoUrl.observe(this, Observer {
it?.let { url ->
//Show image into itemView using Picasso or Glide
Glide.with(itemView.context).load(url).into(binding.awayTeamLogo)
}
})
viewModel.homeLogoUrl.observe(this, Observer {
it?.let { url ->
//Show image into itemView using Picasso or Glide
Glide.with(itemView.context).load(url).into(binding.homeTeamLogo)
}
})
I am building an Android app.
I have a layout that contains a button 'saveButton'. When the user clicks the button, the onClick should call a function in my ViewModel, onSave(). This function requires 2 parameters: the text contents of 2 EditText views that are also present in the same layout.
Basically, the user has edited the name and/or the synopsis and now wants to have the ViewModel update the object's data in the database.
(Part of) my UI (.xml fragment layout):
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".ui.CreateEditRelationFragment">
<data>
<variable
name="createEditRelationViewModel"
type="be.pjvandamme.farfiled.viewmodels.CreateEditRelationViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="#+id/saveButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/saveText"
android:onClick="#{() -> createEditRelationViewModel.onSave(relationNameEditText.getEditText().getText().toString(), relationSynopsisEditText.getEditText().getText().toString())}" />
<Button
android:id="#+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="#string/cancelText"
android:onClick="#{() -> createEditRelationViewModel.onCancel()}" />
<EditText
android:id="#+id/relationSynopsisEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ems="10"
android:gravity="start|top"
android:inputType="textMultiLine" />
<EditText
android:id="#+id/relationNameEditText"
android:layout_width="#dimen/relationNameEditWidth"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name" />
</androidx.constraintlayout.widget.ConstraintLayout>
(Part of) the ViewModel:
class CreateEditRelationViewModel (
private val relationKey: Long?,
val database: RelationDao,
application: Application
): AndroidViewModel(application){
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private var relation = MutableLiveData<Relation?>()
private var _navigateToRelationDetail = MutableLiveData<Boolean>()
val navigateToRelationDetail: LiveData<Boolean>
get() = _navigateToRelationDetail
fun onSave(name: String, synopsis: String){
Timber.i("Got name: " + name + " and synopsis: " + synopsis)
if(relationKey == null){
uiScope.launch{
relation.value = Relation(0, name, synopsis, false)
insert(relation.value!!)
}
}
else{
uiScope.launch{
relation.value?.name = name
relation.value?.synopsis = synopsis
update(relation.value)
}
}
_navigateToRelationDetail.value = true
}
private suspend fun insert(newRelation: Relation){
withContext(Dispatchers.IO){
database.insert(newRelation)
}
}
private suspend fun update(relation: Relation?){
if(relation != null) {
withContext(Dispatchers.IO) {
database.update(relation)
}
}
}
}
I would want this thing to compile so that the onSave() function is called and the contents of the EditTexts passed as parameters.
I cannot manage to pass the text contents of these EditTexts. The compiler throws this error:
[databinding] {"msg":"cannot find method getEditText() in class android.widget.EditText","file":"D:\\ etc.
It does the same thing when I try to access using the .text property directly.
Does anyone know what I'm doing wrong? I'm tearing my hair out.
getEditText() method doesn't exist and you can remove it.
instead of;
relationNameEditText.getEditText().getText().toString()
you can do
relationNameEditText.getText().toString()`
i.e.
https://github.com/dgngulcan/droid-feed/blob/e0d0d5f4af07c5375d42b74e42c55b793319a937/app/src/main/res/layout/fragment_newsletter.xml#L120
Hi I am trying to use data binding and mvvm architecture in my android app. I want to add click listener using data binding in the layout and send the values of username and password edittext to the view model and it will execute the web service and call appropriate method of LoginActivity like startHomeActivity().
Does anyone know how to do this or Am I taking wrong approach ? I have below snippet of code of my activity, layout and view model
LoginActivity.kt
class LoginActivity : BaseActivity(), LoginNavigator {
#Inject
lateinit var loginViewModel: LoginActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activityLoginBinding = DataBindingUtil.setContentView<ActivityLoginBinding>(this, R.layout.activity_login)
}
override fun startHomeActivity() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun startRegistrationActivity() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun startForgotPasswordActivity() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun handleError(throwable: Throwable) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
LoginActivityViewModel.kt
class LoginActivityViewModel {
fun login(email: String, password: String) {
}
/**
* Validate email and password. It checks email and password is empty or not
* and validate email address is correct or not
* #param email email address for login
* #param password password for login
* #return true if email and password pass all conditions else false
*/
fun isEmailAndPasswordValid(email: String, password: String): Boolean {
if (email.isEmpty()) return false
if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) return false
if (password.isEmpty()) return false
return true
}
}
activity_login.xml
<layout>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context="com.app.android.login.LoginActivity"
tools:ignore="missingPrefix">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="#dimen/default_view_margin_bottom_8dp">
<android.support.design.widget.TextInputLayout
android:id="#+id/til_login_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/default_view_margin_right_8dp"
android:layout_marginStart="#dimen/default_view_margin_left_8dp"
android:textColorHint="#color/colorSecondaryText"
app:hintTextAppearance="#style/AppTheme.InputLayoutStyle"
app:layout_constraintBottom_toTopOf="#+id/til_login_password"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/login_email"
android:imeOptions="actionNext"
android:singleLine="true"
android:textColor="#color/colorPrimaryText" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="#+id/til_login_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/default_view_margin_right_8dp"
android:layout_marginStart="#dimen/default_view_margin_left_8dp"
android:textColorHint="#color/colorSecondaryText"
app:hintTextAppearance="#style/AppTheme.InputLayoutStyle"
app:layout_constraintBottom_toTopOf="#+id/btn_login_login"
app:layout_constraintTop_toBottomOf="#+id/til_login_email"
app:layout_constraintVertical_chainStyle="packed">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/login_password"
android:imeOptions="actionDone"
android:singleLine="true"
android:textColor="#color/colorPrimaryText" />
</android.support.design.widget.TextInputLayout>
<Button
android:id="#+id/btn_login_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/default_view_margin_right_8dp"
android:layout_marginStart="#dimen/default_view_margin_left_8dp"
android:layout_marginTop="48dp"
android:text="#string/login_btn_text"
android:textColor="#color/colorWhite"
app:layout_constraintBottom_toTopOf="#+id/textview_login_forgot_password"
app:layout_constraintTop_toBottomOf="#+id/til_login_password"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="#+id/textview_login_forgot_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/default_view_margin_right_8dp"
android:layout_marginStart="#dimen/default_view_margin_left_8dp"
android:layout_marginTop="36dp"
android:gravity="center"
android:text="#string/login_forgot_password"
app:layout_constraintBottom_toTopOf="#+id/btn_login_register"
app:layout_constraintTop_toBottomOf="#+id/btn_login_login"
app:layout_constraintVertical_chainStyle="packed" />
<Button
android:id="#+id/btn_login_register"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/default_view_margin_right_8dp"
android:layout_marginStart="#dimen/default_view_margin_left_8dp"
android:text="#string/login_sign_up"
android:textColor="#color/colorWhite"
app:layout_constraintBottom_toBottomOf="parent" />
</android.support.constraint.ConstraintLayout>
</ScrollView>
</layout>
First of all rename your ViewModel. Its seperated by the View which means the name should be something like LoginViewModel. For this attempt (which is the best available using mvvm pattern in android) you need AAC/LiveData.
Second you should do two-way databinding and assign the ViewModel to your Layout.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewModel" type="...YourVm" />
</data>
<android.support.design.widget.TextInputEditText ...
android:text="#={viewModel.yourField}" />
<Button ... onClick="#{viewModel.onClick}" />
</layout>
That requires a ObservableField<String> in your ViewModel.
Now you want to validate if a click happened by passing the click Event in your activity. For that case you create the Listener in your ViewModel and pass the Data to an Observable.
class LoginViewModel {
val yourField = ObservableField<String>()
val uiEventLiveData = SingleLiveData<Int>()
fun onClick(view:View) {
uiObservable.data = 1 // or any other event
}
}
After this you can use your Activity or Fragment to observe for UIEvents using LiveData (which is lifecycle-aware!).
Now you can use ANY Fragment / Activity which is bound to the ViewModel to observe for UI Events like:
class YourActivity {
private val yourvm by lazy { ViewModelProviders.of(this, viewModelFactory).get(Yourvm::class.java) }
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// ....
binding.viewModel = yourVm
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
yourVm.uiEventLiveData.observe(this, Observer {
when(it) {
1-> { doSomeLoginStuff(yourVm.yourField, ...) } //click happened, do something
else -> .... // unknown ui event
}
})
}
You need the Class SingleLiveData which is a MutableLiveData but nullify your data onec its emitted.
class SingleLiveData<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveData"
}
}
There are several attempts doing that with WeakReferences to avoid Context leak but i highly recommend not doing that. The Reason is that you want to split logic with your view. Having references even if they are lazy or weak breaks the architecture.