diff --git a/README.md b/README.md index 8fd73f9..3002381 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ -# LiveData -Playground for setting up LiveData +# LiveDataBinding Playground + +Architecture: MVVM +Language: Kotlin +Android Architecture Components: LiveData, Data Binding + +A very bare bone example of using MVVM with a couple of android architectural components diff --git a/app/build.gradle b/app/build.gradle index b3e0508..6461a91 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,11 +1,12 @@ apply plugin: 'com.android.application' - apply plugin: 'kotlin-android' - apply plugin: 'kotlin-android-extensions' +apply plugin: "kotlin-kapt" android { compileSdkVersion 28 + buildToolsVersion "28.0.3" + defaultConfig { applicationId "com.example.livedata" minSdkVersion 23 @@ -20,14 +21,23 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + dataBinding { + enabled true + } } + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + + def life_versions = "1.1.1" + + implementation "android.arch.lifecycle:extensions:$life_versions" + annotationProcessor "android.arch.lifecycle:compiler:$life_versions" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 75d5d25..85a551a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,11 @@ - + diff --git a/app/src/main/java/com/example/livedata/BaseViewModel.kt b/app/src/main/java/com/example/livedata/BaseViewModel.kt new file mode 100644 index 0000000..1473d96 --- /dev/null +++ b/app/src/main/java/com/example/livedata/BaseViewModel.kt @@ -0,0 +1,13 @@ +package com.example.livedata + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import android.arch.lifecycle.ViewModel + +open class BaseViewModel : ViewModel() { + protected var LiveData.latestValue: T? + get() = this.value + set(value) { + (this as MutableLiveData).value = value + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livedata/LoginFragment.kt b/app/src/main/java/com/example/livedata/LoginFragment.kt new file mode 100644 index 0000000..b26c42a --- /dev/null +++ b/app/src/main/java/com/example/livedata/LoginFragment.kt @@ -0,0 +1,69 @@ +package com.example.livedata + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.Observer +import android.arch.lifecycle.ViewModelProviders +import android.os.Bundle +import android.support.v4.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import com.example.livedata.LoginErrorType.EmailEmpty +import com.example.livedata.LoginErrorType.PasswordEmpty +import com.example.livedata.LoginErrorType.PasswordShort +import com.example.livedata.databinding.FragmentLoginBinding +import kotlinx.android.synthetic.main.fragment_login.password +import kotlinx.android.synthetic.main.fragment_login.resultEmailAddress +import kotlinx.android.synthetic.main.fragment_login.resultPassword +import kotlinx.android.synthetic.main.fragment_login.username + +class LoginFragment : Fragment() { + private lateinit var viewModel: LoginViewModel + private lateinit var binding: FragmentLoginBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreate(savedInstanceState) + viewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java) + binding = FragmentLoginBinding.inflate(inflater) + return binding.apply { + lifecycleOwner = viewLifecycleOwner + loginViewModel = viewModel + + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + bindViewModel() + } + + private fun bindViewModel() { + username.bindError(viewModel.emailError) + password.bindError(viewModel.passwordError) + + //TODO: remove this logic and the result views when actually implementing login + viewModel.user.observe(this, Observer { + it?.apply { + resultEmailAddress.text = username + resultPassword.text = password + } + }) + } + + private fun EditText.bindError(liveDataError: LiveData) { + liveDataError.observe(viewLifecycleOwner, Observer { + it?.let { loginErrorType -> this.setErrorOnField(loginErrorType) } + }) + } + + private fun EditText.setErrorOnField(errorType: LoginErrorType) { + error = + when (errorType) { + is EmailEmpty -> getString(R.string.empty_email_error) + is PasswordEmpty -> getString(R.string.empty_password_error) + is PasswordShort -> getString(R.string.password_too_short_error) + } + requestFocus() + } +} diff --git a/app/src/main/java/com/example/livedata/LoginViewModel.kt b/app/src/main/java/com/example/livedata/LoginViewModel.kt new file mode 100644 index 0000000..2e6fab8 --- /dev/null +++ b/app/src/main/java/com/example/livedata/LoginViewModel.kt @@ -0,0 +1,33 @@ +package com.example.livedata + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import com.example.livedata.LoginErrorType.EmailEmpty +import com.example.livedata.LoginErrorType.PasswordEmpty +import com.example.livedata.LoginErrorType.PasswordShort + +const val PASSWORD_LENGTH = 5 + +class LoginViewModel : BaseViewModel() { + val password = MutableLiveData() + val username = MutableLiveData() + val emailError: LiveData = MutableLiveData() + val passwordError: LiveData = MutableLiveData() + + var user: LiveData = MutableLiveData() + + fun onClick() { + when { + username.value.isNullOrEmpty() -> emailError.latestValue = EmailEmpty + password.value.isNullOrEmpty() -> passwordError.latestValue = PasswordEmpty + password.value.orEmpty().length < PASSWORD_LENGTH -> passwordError.latestValue = PasswordShort + else -> user.latestValue = User(username.value.orEmpty(), password.value.orEmpty()) + } + } +} + +sealed class LoginErrorType { + object EmailEmpty : LoginErrorType() + object PasswordShort : LoginErrorType() + object PasswordEmpty : LoginErrorType() +} diff --git a/app/src/main/java/com/example/livedata/MainActivity.kt b/app/src/main/java/com/example/livedata/MainActivity.kt index 688971c..936d621 100644 --- a/app/src/main/java/com/example/livedata/MainActivity.kt +++ b/app/src/main/java/com/example/livedata/MainActivity.kt @@ -1,12 +1,16 @@ package com.example.livedata -import android.support.v7.app.AppCompatActivity import android.os.Bundle +import android.support.v7.app.AppCompatActivity class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + val fragment = LoginFragment() + supportFragmentManager + .beginTransaction() + .add(R.id.container, fragment) + .commit() } } diff --git a/app/src/main/java/com/example/livedata/User.kt b/app/src/main/java/com/example/livedata/User.kt new file mode 100644 index 0000000..8e35ca2 --- /dev/null +++ b/app/src/main/java/com/example/livedata/User.kt @@ -0,0 +1,6 @@ +package com.example.livedata + +data class User( + val username: String, + val password: String +) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d46a68a..7454b62 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,21 +1,15 @@ - - - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 0000000..e37479a --- /dev/null +++ b/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + +