In my android app:
dataBinding {
enabled = true
}
in my activity:
class RegistrationActivity : RootActivity() {
private lateinit var viewBinding: RegistrationActivityBinding // use "registration_activity.xml"
private lateinit var registrationViewModel: RegistrationViewModel
private var wallet = Wallet()
companion object {
private val TAG = RegistrationActivity::class.java.name
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Debug.d(TAG, "onCreate: savedInstanceState = $savedInstanceState")
viewBinding = RegistrationActivityBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
viewBinding.setVariable(BR.model, wallet)
viewBinding.executePendingBindings()
init()
}
private fun init() {
viewBinding.buttonRegistration.setOnClickListener {
Debug.d(TAG, "initLogic: click_button, wallet = $wallet")
}
}
}
here my model:
class Wallet() : Serializable {
var email: String? = null
var password: String? = null
override fun toString(): String {
return "\nWallet(email = $email, password = $password)"
}
}
in xml:
<data>
<variable
name="model"
type="com.myproject.model.Wallet" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/common_color_bg">
<include
android:id="#+id/registrationToolbar"
layout="#layout/tool_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="#{#string/registration}" />
<EditText
android:id="#+id/editTextEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="#dimen/default_margin"
android:hint="#string/email"
android:importantForAutofill="no"
android:inputType="textEmailAddress"
android:text="#{model.email}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/registrationToolbar" />
<EditText
android:id="#+id/editTextPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/default_margin"
android:hint="#string/password"
android:importantForAutofill="no"
android:inputType="textPassword"
android:text="#{model.password}"
app:layout_constraintEnd_toEndOf="#+id/editTextEmail"
app:layout_constraintStart_toStartOf="#+id/editTextEmail"
app:layout_constraintTop_toBottomOf="#+id/editTextEmail" />
<EditText
android:id="#+id/editTextPasswordRetype"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/default_margin"
android:hint="#string/retypePassword"
android:importantForAutofill="no"
android:inputType="textPassword"
app:layout_constraintEnd_toEndOf="#+id/editTextEmail"
app:layout_constraintStart_toStartOf="#+id/editTextEmail"
app:layout_constraintTop_toBottomOf="#+id/editTextPassword" />
<com.google.android.material.button.MaterialButton
android:id="#+id/buttonRegistration"
android:layout_width="0dp"
android:layout_height="#dimen/button_height"
android:layout_margin="#dimen/default_margin"
android:layout_marginBottom="#dimen/default_margin"
android:text="#string/registration"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/editTextPasswordRetype" />
<include
android:id="#+id/progressBarLayout"
layout="#layout/progress_bar_layout"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
I input some email in editTextEmail and click button. As result call method:
viewBinding.buttonRegistration.setOnClickListener
Nice.
But here log:
click_button, wallet = Wallet(email = null, password = null)
As you can see the email is null
But I need pass email from xml to activity.
I use viewbinding. Why email not pass to activity via this:
android:text="#{model.email}"
?
You have to use two way binding to back data from View to ViewModel. Use #= to enable two way data binding in your xml like:
android:text="#={model.email}"
Related
So I'm doing a project and I'm completely lost. I have seen how to do data binding with TextViews but I am being asked to do it with EditText Views with Two Way Data Binding. I got up to here so far with it.
The XML File.
<?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="myShoe"
type="com.udacity.shoestore.product.Shoe" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/colorPrimary">
<TextView
android:id="#+id/title_detail_view"
style="#style/title_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:text="#string/add_shoe_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="#+id/shoe_name"
style="#style/login_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ems="10"
android:hint="#string/shoe_name_string"
android:inputType="text"
android:textSize="30sp"
android:text="#={myShoe.name}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/title_detail_view" />
<EditText
android:id="#+id/shoe_size"
style="#style/login_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
android:hint="#string/size_string"
android:inputType="number|numberDecimal"
android:textSize="15sp"
android:text="#={myShoe.size}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/shoe_name" />
<EditText
android:id="#+id/company_name"
style="#style/login_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
android:hint="#string/company_string"
android:inputType="text"
android:textSize="15sp"
android:text="#={myShoe.company}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/shoe_size" />
<EditText
android:id="#+id/shoe_description"
style="#style/login_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
android:hint="#string/description_string"
android:inputType="text"
android:textSize="15sp"
android:text="#={myShoe.description}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/company_name" />
<Button
android:id="#+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="#color/colorPrimaryDark"
android:text="#string/cancel_string"
android:textColor="#android:color/white"
app:layout_constraintBaseline_toBaselineOf="#+id/savee_button"
app:layout_constraintEnd_toStartOf="#+id/savee_button"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="#+id/savee_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="88dp"
android:backgroundTint="#color/colorPrimaryDark"
android:text="#string/save_string"
android:textColor="#android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/shoe_description" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I was told to implement it to a fragment and it should work. But I'm not sure how. Here's the fragment
class ShoeDetailsFragment : Fragment() {
private val viewModel: ActivityViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentShoeDetailsBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_shoe_details,
container, false
)
//initializing the button and clearing the views once canceled
binding.cancelButton.setOnClickListener { v: View ->
v.findNavController().navigateUp()
binding.shoeName.text.clear()
binding.shoeSize.text.clear()
binding.companyName.text.clear()
binding.shoeDescription.text.clear()
}
//initializing the button and saving the info to transfer to the shoeList
binding.saveeButton.setOnClickListener { v: View ->
v.findNavController().navigateUp()
val name = shoe_name.text.toString()
val size = shoe_size.text.toString()
val brand = company_name.text.toString()
val details = shoe_description.text.toString()
viewModel.addShoe(name, size, brand, details)
}
return binding.root
}
}
I am open to any ideas to initialize binding properties so i can use it in both the layout and the fragment. Or am I looking at this the wrong way?
P.S. The XML File is being represented in this fragment
I also did this project for my NanoDegree.
Inside my ViewModel I created 3 variables for each EditText:
MutableLiveData - to update the value inside the viewModel
LiveData to expose value outside viewModel e.g. in a fragment (you won't really need this)
Public Variable to monitor value of MutableLiveData and expose this to your xml thus achieving the 2-Way Binding.
Then I and would create a Shared ViewModel to share data between ShoeDetailsFragment and ShoeListingFragment .
Inside the SharedViewModel
I created 3 variables for each EditText (this is just the first 2 Edittexts):
class MySharedViewModel : ViewModel() {
private val _name = MutableLiveData<String>()
val name: LiveData<String>
get() = _name
var edShoeName = ""
private val _size = MutableLiveData<Double>()
val size: LiveData<Double>
get() = _size
var edSize = ""
......}
For the xml I did exactly what you have done but used the 3rd variable for the 2-Way Data Binding:
<EditText
android:id="#+id/shoe_name"
style="#style/login_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ems="10"
android:hint="#string/shoe_name_string"
android:inputType="text"
android:textSize="30sp"
android:text="#={mySharedViewModel.edShoeName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/title_detail_view" />
<EditText
android:id="#+id/shoe_size"
style="#style/login_style"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
android:hint="#string/size_string"
android:inputType="number|numberDecimal"
android:textSize="15sp"
android:text="#={mySharedViewModel.edCompany}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/shoe_name" />
I have seen you have included this line of code on your ShoeDetailFragment code:
binding.saveeButton.setOnClickListener { v: View -> ....}
In my case I did it inside SharedViewModel instead:
fun onSaveButtonClick() {
//check value entered for size and set it to 0 if blank
if (edSize == "") {
edSize = "0"
}
//update MutableLiveData with values read live from the EditText
_name.value = edShoeName
_size.value = edSize.toDouble()
//save shoeObject to the _shoeList MutableLiveData
_shoeList.value?.add(Shoe(edShoeName, edSize.toDouble(), edCompany, edDescription))
}
Using DataBinding I moved the onClick bit to xml:
<Button
android:id="#+id/buttonSave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="75dp"
android:layout_marginBottom="75dp"
android:background="#drawable/custom_button_background"
android:onClick="#{()->sharedViewModel.onSaveButtonClick()}"
You can also refer to my project.
Using Databinding, ViewModel, LiveData (MVVM).
In my Layout there's a form to add Employee Details.
layout:
<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="viewModelAddEmployee"
type="com.app.roomemployeedemo.viewmodel.AddEmployeeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/edt_lastname"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.AddEmployeeActivity">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:text="#{viewModelAddEmployee.firstname}"
android:id="#+id/edt_firstname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:ems="10"
android:hint="Jaimin"
android:inputType="textPersonName"
android:maxLength="30"
android:maxLines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:text="#{viewModelAddEmployee.lastname}"
android:id="#+id/tv_lastname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:ems="10"
android:hint="Modi"
android:inputType="textPersonName"
android:maxLength="30"
android:maxLines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/edt_firstname" />
<EditText
android:text="#{viewModelAddEmployee.age}"
android:id="#+id/edt_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:ems="10"
android:hint="25"
android:inputType="textPersonName"
android:maxLength="3"
android:maxLines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/tv_lastname" />
<EditText
android:text="#{viewModelAddEmployee.gender}"
android:id="#+id/edt_gender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:ems="10"
android:hint="Gender (M/F, m/f)"
android:inputType="textPersonName"
android:maxLength="1"
android:maxLines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/edt_age" />
<EditText
android:text="#{viewModelAddEmployee.salary}"
android:id="#+id/edt_salary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:ems="10"
android:hint="Salary (50000)"
android:inputType="textPersonName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/edt_gender" />
<Button
android:onClick="#{(v) -> viewModelAddEmployee.onAddEmployeeClick(v)}"
android:id="#+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="40dp"
android:text="ADD NOW"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/edt_salary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
Below is the ViewModel class for the same :
class AddEmployeeViewModel(mContext: Context) : ViewModel(){
var mContext=mContext
lateinit var firstname:MutableLiveData<String>
lateinit var lastname:MutableLiveData<String>
lateinit var age:MutableLiveData<Int>
lateinit var gender:MutableLiveData<Char>
lateinit var salary:MutableLiveData<Int>
lateinit var repositoryEmployee:EmployeeRepository
init{
repositoryEmployee= EmployeeRepository(mContext)
}
fun onAddEmployeeClick(view: View)
{
repositoryEmployee.onAddEmployeeClick(mContext,firstname,lastname,age,gender,salary)
}
}
Initialized Viewmodel and binding utility in Activity class as below :
class AddEmployeeActivity : AppCompatActivity() {
lateinit var viewModelAddEmployee: AddEmployeeViewModel
lateinit var binding: ActivityAddEmployeeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModelAddEmployee = ViewModelProvider(this,AddEmployeeFactory(this#AddEmployeeActivity)).get(AddEmployeeViewModel::class.java)
binding = DataBindingUtil.setContentView(
this#AddEmployeeActivity,
R.layout.activity_add_employee
)
binding?.setLifecycleOwner(this)
binding?.viewModelAddEmployee = viewModelAddEmployee
}
}
But, Getting below Error :
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property gender has not been initialized
at com.app.roomemployeedemo.viewmodel.AddEmployeeViewModel.getGender(AddEmployeeViewModel.kt:16)
What might be the issue? Especially with Char type gender!!
In kotlin you must initialize lateinit property. Add below line to oncreate -
gender = MutableLiveData<Char>()
You are declaring your live data objects as lateinit var which means that you have to initialize them later before referring to them. You need to remove lateinit and initialize when creating the ViewModel or initialize each live data field in your init block like here:
firstname = MutableLiveData<>()
Normally, properties declared as having a non-null type must be initialized in the constructor. However, fairly often this is not convenient. For example, properties can be initialized through dependency injection, or in the setup method of a unit test. In this case, you cannot supply a non-null initializer in the constructor, but you still want to avoid null checks when referencing the property inside the body of a class.
To handle this case, you can mark the property with the lateinit modifier:
Sample
public class MyTest {
lateinit var subject: TestSubject
#SetUp fun setup() {
subject = TestSubject()
}
#Test fun test() {
subject.method() // dereference directly
}
}
In your case, values must be loaded into variables in ViewModel.
In you case;
class AddEmployeeViewModel(mContext: Context) : ViewModel(){
var mContext=mContext
lateinit var firstname:MutableLiveData<String>
lateinit var lastname:MutableLiveData<String>
lateinit var age:MutableLiveData<Int>
lateinit var gender:MutableLiveData<Char>
lateinit var salary:MutableLiveData<Int>
lateinit var repositoryEmployee:EmployeeRepository
init{
repositoryEmployee= EmployeeRepository(mContext)
lastname = MutableLiveData<String>()
firstname = MutableLiveData<String>()
age = MutableLiveData<Int>()
gender = MutableLiveData<Char>()
salary = MutableLiveData<Int>()
}
fun onAddEmployeeClick(view: View)
{
repositoryEmployee.onAddEmployeeClick(mContext,firstname,lastname,age,gender,salary)
}
}
Best Practice
private val _sampleVar = MutableLiveData<String>()
val sampleVar: LiveData<String>
get() = _sampleVar
What you can do is this
fun returnName(): LiveData<String> {
if (firstname == null) {
firstname = MutableLiveData()
//call some function to set value to the variable
}
return firstname
}
This would ensure that you don't create multiple instances of that list and it would return the same instance eveytime.
You can implement it on any variable you want
In my Fragments layout I have a button which is using the XML onClick expression to call a method in the corresponding ViewModel.
The problem is that upon building the project, the compiler throws following error:
error: cannot find symbol
import com.aresid.myapp.databinding.FragmentLoginBindingImpl;
^
symbol: class FragmentLoginBindingImpl
location: package com.aresid.myapp.databinding
The thing is that, when I remove the parameters from the functions signature and the functions call in the XML, the app builds without problems.
My expectation is that, when I click the login button, the method in the ViewModel would be called, logging the emailField.text and passwordField.text.
The layout file:
<?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="loginViewModel"
type="com.aresid.myapp.login.LoginViewModel"
/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="#dimen/fragmentPaddingMedium"
tools:context=".login.LoginFragment"
>
<ImageView
android:id="#+id/app_logo"
android:layout_width="#dimen/imageViewAppLogoSize"
android:layout_height="#dimen/imageViewAppLogoSize"
android:layout_marginTop="4dp"
android:contentDescription="#string/my_app_logo"
android:src="#drawable/my_app_logo_144"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/email_field_layout"
style="#style/Widget.MyApp.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true"
app:layout_constraintTop_toBottomOf="#+id/app_logo"
>
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/email_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/email"
android:inputType="textEmailAddress"
android:maxLines="1"
android:singleLine="true"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/password_field_layout"
style="#style/Widget.MyApp.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true"
app:layout_constraintTop_toBottomOf="#+id/email_field_layout"
app:passwordToggleEnabled="true"
>
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/password_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/password"
android:imeActionId="6"
android:imeActionLabel="#string/common_signin_button_text"
android:imeOptions="actionUnspecified"
android:inputType="textPassword"
android:maxLines="1"
android:singleLine="true"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="#+id/login_button"
style="#style/Widget.MyApp.ContainedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/log_in"
android:onClick="#{() -> loginViewModel.onLoginButtonClicked(emailField.text, passwordField.text)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/password_field_layout"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="13dp"
android:text="#string/sign_up_using_these_colon"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="#+id/signup_button_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
<LinearLayout
android:id="#+id/signup_button_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="13dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
>
<androidx.appcompat.widget.AppCompatImageButton
android:id="#+id/email_signup_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6.5dp"
android:background="#drawable/round_button_background"
android:src="#drawable/ic_email_24dp"
/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="#+id/google_signup_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6.5dp"
android:background="#drawable/round_button_background"
android:src="#drawable/ic_google_favicon_24dp"
/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The corresponding ViewModel:
package com.aresid.myapp.login
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import timber.log.Timber
/**
* Created on: 27/04/2020
* For Project: MyApp
* Author: René Spies
* Copyright: © 2020 ARES ID
*/
class LoginViewModel: ViewModel() {
// LiveData for the login email
private val _email = MutableLiveData<String>()
val email: LiveData<String>
get() = _email
// LiveData for the login password
private val _password = MutableLiveData<String>()
val password: LiveData<String>
get() = _password
// LiveData for the login button
private val _wantLogin = MutableLiveData<Boolean>()
val wantLogin: LiveData<Boolean>
get() = _wantLogin
init {
Timber.d("init: called")
// TODO: init: init objects here
// Init email LiveData
_email.value = ""
// Init password LiveData
_password.value = ""
// Init wantLogin LiveData
_wantLogin.value = false
}
fun onLoginButtonClicked(email: String, password: String) {
Timber.d("onLoginButtonClicked: called")
Timber.d("email = $email\npassword = $password")
// TODO: onLoginButtonClicked: log in user
}
}
And the Fragments onCreateView():
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Timber.d("onCreateView: called")
// Define LoginViewModel
loginViewModel = ViewModelProvider(this).get(LoginViewModel::class.java)
// Define FragmentLoginBinding and inflate the layout
binding = FragmentLoginBinding.inflate(inflater, container, false)
// TODO: onCreateView: code goes here
// Let the data binder know about the LoginViewModel
binding.loginViewModel = loginViewModel
// Return the inflated layout
return binding.root
}
Try this:
Use Two-way Databinding for your e-mail and password. By this your ViewModel fields will change automatically after user changes them in UI:
android:text="#={viewmodel.password}"
Your function "onLoginButtonClicked" can now be declared without parameters (these values are hold now as fields of ViewModel so you can get them freely inside function).
In your xml change the onClick as well:
android:onClick="#{() -> loginViewModel.onLoginButtonClicked()}"
My click listener are not working even if I check with a Toast inside of them, it use to work but after I made a couple of change in my viewModel it stop working, I can't figure out what went wrong. This happen in my detail activity only, but work on the recyclerview that call this detail activity via intent. I'm using Viewmodel, Livedata, databinding and Room. The recyclerview and the detail view are using the same viewmodel.
This is the code of my Detail activity:
class BuyDetailActivity : AppCompatActivity() {
private lateinit var sharedViewModel: BuySharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lateinit var buy: Buy
sharedViewModel = ViewModelProviders.of(this).get(BuySharedViewModel::class.java)
val position = intent.getIntExtra("position", 0)
sharedViewModel.allBuys.observe(this, Observer<List<Buy>> { buys ->
buy = buys[position]
val binding: com.example.drake.kunuk.databinding.ActivityBuyDetailBinding =
DataBindingUtil.setContentView(this, com.example.drake.kunuk.R.layout.activity_buy_detail)
binding.buy = buy
val agentNumber = buy.agentNumber
bnvContactAgent.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
com.example.drake.kunuk.R.id.action_call -> {
val callNumberUri = Uri.parse("tel:$agentNumber")
val callIntent = Intent(Intent.ACTION_DIAL, callNumberUri)
startActivity(callIntent)
}
com.example.drake.kunuk.R.id.action_sms -> {
val smsNumberUri = Uri.parse("sms:$agentNumber")
val smsIntent = Intent(Intent.ACTION_SENDTO, smsNumberUri)
startActivity(smsIntent)
}
com.example.drake.kunuk.R.id.action_email -> {
val uriText = "mailto:xxxxxxxx#gmail.com" +
"?subject=" + Uri.encode("I'm interested in $agentNumber") +
"&body=" + Uri.encode("Hello, ")
val uri = Uri.parse(uriText)
val sendIntent = Intent(Intent.ACTION_SENDTO)
sendIntent.data = uri
startActivity(Intent.createChooser(sendIntent, "Send email"))
}
}
false
}
// set animation duration via code, but preferable in your layout files by using the animation_duration attribute
expandableTextView.setAnimationDuration(750L)
// set interpolators for both expanding and collapsing animations
expandableTextView.setInterpolator(OvershootInterpolator())
// or set them separately
expandableTextView.expandInterpolator = OvershootInterpolator()
expandableTextView.collapseInterpolator = OvershootInterpolator()
// toggle the ExpandableTextView
buttonToggle.setOnClickListener {
buttonToggle.setText(if (expandableTextView.isExpanded) com.example.drake.kunuk.R.string.more else com.example.drake.kunuk.R.string.less)
expandableTextView.toggle()
}
// but, you can also do the checks yourself
buttonToggle.setOnClickListener {
if (expandableTextView.isExpanded) {
expandableTextView.collapse()
buttonToggle.setText(com.example.drake.kunuk.R.string.more)
} else {
expandableTextView.expand()
buttonToggle.setText(com.example.drake.kunuk.R.string.less)
}
}
//Open photoView activity when clicked
ivHouseDetail.setOnClickListener {
applicationContext
.startActivity(
Intent(
applicationContext,
ViewPagerActivity::class.java
)
.putExtra("imageList", buy.propertyImage)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
})
}
}
Here's my SharedViewmodel, use by both the fragment calling the detail activity and the detail activity.
class BuySharedViewModel(application: Application) : AndroidViewModel(application) {
private val repository: BuyRepository
var allBuys: LiveData<List<Buy>>
init {
val buyDao = KunukRoomDatabase.getDatabase(application, viewModelScope).buyDao()
val buyRemote = BuyRemote()
repository = BuyRepository.getInstance(buyDao , buyRemote)
//Use async because it return a result
viewModelScope.async { getAllBuys() }
allBuys = buyDao.loadAllBuys()
}
private suspend fun getAllBuys() {
repository.getBuys()
}
}
And finally this is the xml of the detail activity:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="buy" type="com.example.drake.kunuk.data.model.Buy"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_height="match_parent" android:layout_width="match_parent"
>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bnvContactAgent"
android:layout_width="match_parent"
android:background="#color/colorPrimary"
app:itemIconTint="#color/colorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:labelVisibilityMode="unlabeled"
app:menu="#menu/bottom_nav_contact_agent"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_bias="1.0"/>
<ScrollView
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_height="0dp" android:layout_width="0dp"
app:layout_constraintBottom_toTopOf="#+id/bnvContactAgent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".ui.buy.BuyDetailActivity">
<ImageView
android:layout_width="match_parent"
android:layout_height="230dp"
tools:srcCompat="#tools:sample/backgrounds/scenic"
android:id="#+id/ivHouseDetail"
android:scaleType="centerCrop"
android:contentDescription="#string/house"
app:imageUrl="#{buy.propertyImage}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
app:formatToUSD="#{buy.price}"
android:id="#+id/tvPriceDetail"
android:textStyle="bold"
android:textColor="#color/colorPrimaryText"
android:textSize="20sp"
tools:ignore="HardcodedText"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="#+id/ivHouseDetail" android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:text="#{buy.address}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvAddressDetail"
android:textColor="#color/colorSecondaryText"
app:layout_constraintTop_toBottomOf="#+id/tvPriceDetail"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"/>
<TextView
android:text="#{Integer.toString(buy.numberOfRoom)}"
android:maxLength="3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvBedroom"
android:layout_marginTop="12dp"
android:textStyle="bold"
android:textColor="#color/colorPrimaryText"
app:layout_constraintTop_toBottomOf="#+id/tvAddressDetail"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" app:srcCompat="#drawable/ic_bed"
android:id="#+id/ivBedroom"
android:contentDescription="#string/bedroom_icon"
app:layout_constraintStart_toEndOf="#+id/tvBedroom"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="#+id/tvAddressDetail"/>
<TextView
android:text="#{Integer.toString(buy.numberOfBath)}"
android:maxLength="3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvBathroom"
android:textStyle="bold"
android:textColor="#color/colorPrimaryText"
app:layout_constraintStart_toEndOf="#+id/ivBedroom"
android:layout_marginStart="20dp" app:layout_constraintTop_toTopOf="#+id/ivBedroom"
android:layout_marginTop="4dp"/>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp" app:srcCompat="#drawable/ic_bathtub"
android:id="#+id/imageView2"
android:contentDescription="#string/bathroom_icon"
app:layout_constraintStart_toEndOf="#+id/tvBathroom"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="#+id/tvAddressDetail"/>
<TextView
android:text="#{Integer.toString(buy.numberOfCar)}"
android:maxLength="3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvGarage"
android:textStyle="bold"
android:textColor="#color/colorPrimaryText"
app:layout_constraintStart_toEndOf="#+id/imageView2"
android:layout_marginStart="20dp" app:layout_constraintTop_toTopOf="#+id/imageView2"
android:layout_marginTop="4dp"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" app:srcCompat="#drawable/ic_garage"
android:id="#+id/imageView3"
android:contentDescription="#string/garage_icon"
app:layout_constraintStart_toEndOf="#+id/tvGarage"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="#+id/tvAddressDetail"/>
<View
android:id="#+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
tools:layout_editor_absoluteY="281dp" tools:layout_editor_absoluteX="8dp"
app:layout_constraintBottom_toTopOf="#+id/tvDescTitle"
android:layout_marginBottom="4dp"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvDescTitle"
android:text="#string/description"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:textStyle="bold"
android:textSize="18sp"
android:textColor="#color/colorSecondaryText"
app:layout_constraintTop_toBottomOf="#+id/imageView3"
app:layout_constraintBottom_toTopOf="#id/expandableTextView"
app:layout_constraintStart_toStartOf="parent"/>
<at.blogc.android.views.ExpandableTextView
android:text="#{buy.propertyDesc}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/expandableTextView"
android:textColor="#color/colorSecondaryText"
android:maxLines="5"
android:ellipsize="end"
app:animation_duration="750"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintTop_toBottomOf="#+id/tvDescTitle"
app:layout_constraintEnd_toEndOf="parent"/>
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/buttonToggle"
style="#style/Widget.MaterialComponents.Button.OutlinedButton"
android:text="#string/more"
app:goneUnless="#{true}"
app:layout_constraintTop_toBottomOf="#+id/expandableTextView"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="8dp"/>
<TextView
app:photoCounter="#{buy.propertyImage}"
android:background="#99000000"
android:elevation="4dp"
android:padding="4dp"
android:textColor="#ffafffff"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="#+id/ivHouseDetail"
android:layout_margin="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" tools:layout_editor_absoluteY="189dp"
tools:layout_editor_absoluteX="323dp" android:id="#+id/tvPhotoCounter"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
This code is my repository class, where I use the coroutines.
class BuyRepository (private val buyDao: BuyDao, private val buyRemote: BuyRemote) {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job)
companion object {
//For singleton instantiation
#Volatile private var instance: BuyRepository? = null
fun getInstance(buyDao: BuyDao, buyRemote: BuyRemote) =
instance ?: synchronized(this) {
instance ?: BuyRepository(buyDao, buyRemote)
.also { instance = it}
}
}
suspend fun getBuys(){
refresh()
}
private suspend fun refresh(){
val list = scope.async {buyRemote.loadBuys()}
list.await().forEach { buy -> insert(buy) }
}
//#WorkerThread
private fun insert(buy: Buy) {
buyDao.insertBuy(buy)
}
}
// toggle the ExpandableTextView
buttonToggle.setOnClickListener { // <- Set listener here
buttonToggle.setText(if (expandableTextView.isExpanded) com.example.drake.kunuk.R.string.more else com.example.drake.kunuk.R.string.less)
expandableTextView.toggle()
}
// but, you can also do the checks yourself
buttonToggle.setOnClickListener { // <- And overwritten here - seems bad
if (expandableTextView.isExpanded) {
expandableTextView.collapse()
buttonToggle.setText(com.example.drake.kunuk.R.string.more)
} else {
expandableTextView.expand()
buttonToggle.setText(com.example.drake.kunuk.R.string.less)
}
}
Looks to me like you're overwriting the click listener. So ... don't do that :)
I want use 2-way dataBinding by LiveData. but change editText value not update textView that show user.name.
what is wrong by my code? I use android studio 3.3 canary 3 and enable data binding v2 in gradle.properties by this code:
android.databinding.enableV2=true
model data class:
data class User(var name: String)
viewModel class:
class MainViewModel : ViewModel() {
val user = MutableLiveData<User>()
init {
user.value = User("ali")
}
}
mainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activityMainBinding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
activityMainBinding.setLifecycleOwner(this)
activityMainBinding.viewModel = viewModel
}
}
<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="com.example.hoseinkelidari.databindingsample.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="#+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="60dp"
android:layout_marginEnd="8dp"
android:text="#={viewModel.user.name}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="#{viewModel.user.name}"
app:layout_constraintBottom_toTopOf="#+id/editText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Update:
I found solution at this link:
LiveData update on object field change
I set this change and it work:
class User : BaseObservable() {
#get:Bindable
var firstName: String? = null
set(name) {
field = firstName
notifyPropertyChanged(BR.firstName)
}
}
class MainViewModel : ViewModel() {
var user = CustomMutableLiveData<User>()
init {
user.value = User()
}
}
and add new
class CustomMutableLiveData<T : BaseObservable> : MutableLiveData<T>() {
internal var callback: Observable.OnPropertyChangedCallback = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
//Trigger LiveData observer on change of any property in object
value = value
}
}
override fun setValue(value: T?) {
super.setValue(value)
//listen to property changes
value!!.addOnPropertyChangedCallback(callback)
}
}
I think the problem is that you only change the value of user's name, not the value of LiveData, so textview is not changing according to it.
Take a look at some change in your code, and 2-way dataBinding is worked in this case.
Change the ViewModel
class MainViewModel : ViewModel() {
val user = MutableLiveData<String>()
init {
user.value = "Ali"
}
}
and change the layout
<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="com.example.hoseinkelidari.databindingsample.MainViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="#+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="60dp"
android:layout_marginEnd="8dp"
android:text="#={viewModel.user}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="#{viewModel.user}"
app:layout_constraintBottom_toTopOf="#+id/editText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</layout>