Skip to content

Instantly share code, notes, and snippets.

@liamkernighan
Last active February 8, 2020 11:31
Show Gist options
  • Save liamkernighan/a63b2896e0601abe843ad7768041e33a to your computer and use it in GitHub Desktop.
Save liamkernighan/a63b2896e0601abe843ad7768041e33a to your computer and use it in GitHub Desktop.
LiveData MVI example
package com.nasladdin.partner.activities.login
import android.os.Bundle
import android.view.KeyEvent
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.nasladdin.partner.R
import com.nasladdin.partner.helpers.SnackbarFacade
/**
* Переадресовываем сюда при на всех Activity и фрагментах, защищённых аутентификацией.
*/
class LoginActivity : AppCompatActivity() {
private lateinit var viewModel: LoginViewModel
private lateinit var snackbarFacade: SnackbarFacade
private lateinit var store: LoginStore
private lateinit var view: LoginView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.login_activity)
initViewModel()
view = LoginView(this)
snackbarFacade = SnackbarFacade(this, findViewById(R.id.login_activity_intro_text))
store = LoginStore(viewModel, lifecycle)
setListeners()
}
private fun initViewModel() {
viewModel = ViewModelProvider(this).get(LoginViewModel::class.java)
viewModel.state.observe(this, Observer { render(it) })
viewModel.viewEffect.observe(this, Observer { handleEffect(it) })
}
private fun render(viewState: LoginViewState) = when (viewState) {
is LoginViewState.AwaitingCredentials -> {
view.setLoginButtonEnabled(true)
view.setLoaderEnabled(false)
}
is LoginViewState.IsLoading -> {
view.setLoginButtonEnabled(false)
view.setLoaderEnabled(true)
}
is LoginViewState.LoggedInSuccessfully -> {
finish()
}
}
private fun handleEffect(viewEffect: LoginViewEffect) = snackbarFacade.warning(viewEffect.message)
private fun setListeners() {
view.button.setOnClickListener {
store.onLoginButtonClick(view.loginText, view.passwordText)
}
// todo в хелпер
view.passwordEditField.setOnKeyListener { v, keyCode, event ->
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
store.onLoginButtonClick(view.loginText, view.passwordText)
true
}
false
}
}
}
package com.nasladdin.partner.activities.login
import androidx.lifecycle.Lifecycle
import com.nasladdin.partner.application.GlobalApplication
import com.nasladdin.partner.boilerplate.StoreBase
import com.nasladdin.partner.helpers.sharedpreferences.LoginManager
import com.nasladdin.partner.repos.remote.fuel.FuelAuthRepository
import com.nasladdin.partner.repos.remote.fuel.HttpError
import kotlinx.coroutines.*
class LoginStore(private val vm: LoginViewModel, lifecycle: Lifecycle): StoreBase(lifecycle) {
private val authRepository: FuelAuthRepository = GlobalApplication.dependencyContainer.fuelAuthRepository
private val loginManager: LoginManager = GlobalApplication.dependencyContainer.loginManager
fun onLoginButtonClick(login: String, password: String) = launch {
if (login.isBlank() || password.isBlank()) {
vm.viewEffect.value = LoginViewEffect("Заполните, пожалуйста, логин и пароль")
return@launch
}
vm.state.value = LoginViewState.IsLoading
withContext(Dispatchers.IO) {
authRepository.login(login, password)
}
.mapLeft {
vm.viewEffect.value = LoginViewEffect(it.errorHandler())
vm.state.value = LoginViewState.AwaitingCredentials
}
.map {
loginManager.saveCredentialsToPreferences(it.accessToken, it.refreshToken)
vm.state.value = LoginViewState.LoggedInSuccessfully
}
}
private fun HttpError.errorHandler() = when (this) {
is HttpError.WrongCredentialsException -> "Логин или пароль неверные"
is HttpError.NetworkUnreachableException -> "Нет связи с сервером Насладдина. Возможно, отсутствует соединение с интернетом"
else -> "Неизвестная ошибка, пожалуйста, попробуйте позже $this"
}
}
package com.nasladdin.partner.activities.login
import android.view.View
import android.widget.Button
import android.widget.EditText
import com.nasladdin.partner.R
class LoginView(private val activity: LoginActivity) {
val button: Button = activity.findViewById(R.id.login_login_button)
val passwordEditField: EditText = activity.findViewById(R.id.login_password_text)
fun setLoginButtonEnabled(enabled: Boolean) {
val button = activity.findViewById<Button>(R.id.login_login_button)
button.isEnabled = enabled
val colorCode = if (enabled) R.color.bootstrap_success else R.color.button_default
button.setBackgroundColor(activity.resources.getColor(colorCode))
}
fun setLoaderEnabled(enabled: Boolean) {
val loader = activity.findViewById<View>(R.id.progress_bar)
loader.visibility = if (enabled) View.VISIBLE else View.GONE
}
val loginText: String
get() {
val loginText = activity.findViewById<EditText>(R.id.login_login_text)
return loginText.text.toString()
}
val passwordText: String
get() {
val passwordText = activity.findViewById<EditText>(R.id.login_password_text)
return passwordText.text.toString()
}
}
package com.nasladdin.partner.activities.login
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nasladdin.partner.application.GlobalApplication
import com.nasladdin.partner.boilerplate.BaseViewModel
import com.nasladdin.partner.helpers.sharedpreferences.LoginManager
import com.nasladdin.partner.repos.remote.fuel.FuelAuthRepository
import com.nasladdin.partner.repos.remote.fuel.HttpError
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class LoginViewModel : ViewModel() {
val state = MutableLiveData<LoginViewState>()
val viewEffect = MutableLiveData<LoginViewEffect>()
}
sealed class LoginViewState {
object AwaitingCredentials: LoginViewState()
object IsLoading: LoginViewState()
object LoggedInSuccessfully: LoginViewState()
}
data class LoginViewEffect(val message: String)
package com.nasladdin.partner.boilerplate
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
abstract class StoreBase(private val lifecycle: Lifecycle): CoroutineScope, LifecycleObserver {
init {
@Suppress("LeakingThis")
lifecycle.addObserver(this)
}
public val compositeJob = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + compositeJob
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
private fun onDestroy() {
compositeJob.cancel()
lifecycle.removeObserver(this)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment