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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With