Skip to content

Instantly share code, notes, and snippets.

@Pooh3Mobi
Last active January 14, 2019 12:16
Show Gist options
  • Save Pooh3Mobi/2cdb9ec1f6836701ac523c1819eacb95 to your computer and use it in GitHub Desktop.
Save Pooh3Mobi/2cdb9ec1f6836701ac523c1819eacb95 to your computer and use it in GitHub Desktop.
Login form sample with Functional Reactive Programming + DataBinding by Kotlin + Android
<?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>
<import type="mobi.pooh3.androidxsimplereactiveporgramming.loginform.Msg"/>
<import type="io.reactivex.processors.PublishProcessor"/>
<variable
name="viewModel"
type="mobi.pooh3.androidxsimplereactiveporgramming.loginform.LoginFormViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topic"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".loginform.LoginFormFragment">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="e-mail"
app:layout_constraintBottom_toTopOf="@+id/editText"
app:layout_constraintStart_toStartOf="@+id/editText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<EditText
android:id="@+id/editText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ems="10"
android:hint="enter e-mail address"
android:inputType="textEmailAddress"
android:onTextChanged="@{(text, start, before, count) -> viewModel.event.onNext(Msg.email(text))}"
app:layout_constraintBottom_toTopOf="@+id/textView2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="pass word"
app:layout_constraintBottom_toTopOf="@+id/editText2"
app:layout_constraintStart_toStartOf="@+id/editText"
app:layout_constraintTop_toBottomOf="@+id/editText" />
<EditText
android:id="@+id/editText2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ems="10"
android:hint="enter pass word"
android:inputType="textPassword"
android:onTextChanged="@{(text, start, before, count) -> viewModel.event.onNext(Msg.passWord(text))}"
app:layout_constraintBottom_toTopOf="@+id/button2"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="64dp"
android:layout_marginBottom="8dp"
android:text="Button"
android:onClick="@{() -> viewModel.event.onNext(Msg.Submit.INSTANCE)}"
android:enabled="@{viewModel.submitEnable}"
app:layout_constraintBottom_toTopOf="@+id/guideline"
app:layout_constraintEnd_toEndOf="@+id/editText2"
app:layout_constraintTop_toBottomOf="@+id/editText2" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="344dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class LoginFormFragment : Fragment() {
companion object {
fun newInstance() = LoginFormFragment()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
requireActivity().title = "Simple Login Form"
return bindingWith(LoginFormFragmentBinding.inflate(inflater)) {
viewModel = provideViewModel()
}.also {
requireNotNull(it.viewModel).submitResult.observe(this@LoginFormFragment, Observer {
Toast.makeText(requireContext(), "submitted!", Toast.LENGTH_SHORT).apply {
setGravity(Gravity.CENTER, 0, -300)
}.show()
})
}.root
}
}
sealed class Msg {
data class Email(val email: CharSequence) : Msg()
data class PassWord(val pw: CharSequence) : Msg()
object Submit : Msg()
companion object {
@JvmStatic fun email(email: CharSequence) = Email(email)
@JvmStatic fun passWord(pw: CharSequence) = PassWord(pw)
}
}
data class Model(
val email: CharSequence,
val passWord: CharSequence,
val response: Result
)
sealed class Result {
object Nothing : Result()
object Success : Result()
object Error : Result()
}
@Suppress("MoveLambdaOutsideParentheses")
class LoginFormViewModel : ViewModel() {
// ユーザからのイベントを通知
val event = publishProcessor<Msg>()
// ユーザからのイベント処理結果を観測する
val submitEnable: LiveData<Boolean>
val submitResult: LiveData<Result>
init {
val state = behaviorProcessor(Model(email = "", passWord = "", response = Result.Nothing))
// Msg Model -> Model, distinctUntilChangedで連打対策
update(event.distinctUntilChanged(), state, { msg, model ->
when (msg) {
is Msg.Email -> model.copy(email = msg.email)
is Msg.PassWord -> model.copy(passWord = msg.pw)
// APIの処理通信結果を入れる。今回はSuccess固定
is Msg.Submit -> model.copy(response = Result.Success)
}
})
// ボタンを有効にする条件の処理を挟む(現在は二つのパラメータが空ではないこと)
submitEnable = state.map { it.email.isNotBlank() && it.passWord.isNotBlank() }.toLiveData()
submitResult = state.filter { it.response !is Result.Nothing }.map { it.response }.toLiveData()
}
}
class LoginFormFragment : Fragment() {
companion object {
fun newInstance() = LoginFormFragment()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
requireActivity().title = "Simple Login Form"
return bindingWith(LoginFormFragmentBinding.inflate(inflater)) {
viewModel = provideViewModel()
}.also {
requireNotNull(it.viewModel).submitResult.observe(this@LoginFormFragment, Observer {
Toast.makeText(requireContext(), "submitted!", Toast.LENGTH_SHORT).apply {
setGravity(Gravity.CENTER, 0, -300)
}.show()
})
}.root
}
}
sealed class Msg {
data class Email(val email: CharSequence) : Msg()
data class PassWord(val pw: CharSequence) : Msg()
object Submit : Msg()
companion object {
@JvmStatic fun email(email: CharSequence) = Email(email)
@JvmStatic fun passWord(pw: CharSequence) = PassWord(pw)
}
}
sealed class Result {
object Nothing : Result()
object Success : Result()
object Error : Result()
}
data class LFInputs(
val event: Flowable<Msg>
) : Inputs
data class LFOutputs(
val submitEnable: Flowable<Boolean>,
val submitResult: Flowable<Result>
) : Outputs
interface LoginForm : Processor<LFInputs, LFOutputs>
@Suppress("MoveLambdaOutsideParentheses")
class LoginFormViewModel : ViewModel(), LoginForm {
// ユーザからのイベントを通知
val event = publishProcessor<Msg>()
// ユーザからのイベント処理結果を観測する
val submitEnable: LiveData<Boolean>
val submitResult: LiveData<Result>
init {
val outputs = create(LFInputs(event = event.distinctUntilChanged()))
submitEnable = outputs.submitEnable.toLiveData()
submitResult = outputs.submitResult.toLiveData()
}
override fun create(inputs: LFInputs): LFOutputs {
val allValid = checkInputs(inputs.event)
val submitResult = attemptLogin(inputs.event)
return LFOutputs(
submitEnable = allValid,
submitResult = submitResult
)
}
companion object {
private fun checkInputs(
sEvent: Flowable<Msg>
): Flowable<Boolean> {
val emailAndPassWord = behaviorProcessor(false to false)
// ボタンを有効にする条件の処理を挟む(現在は二つのパラメータが空ではないこと)
update(sEvent, emailAndPassWord, { msg, pair ->
when (msg) {
is Msg.Email -> pair.copy(first = msg.email.isNotBlank())
is Msg.PassWord -> pair.copy(second = msg.pw.isNotBlank())
is Msg.Submit -> pair
}
})
return emailAndPassWord
.distinctUntilChanged()
.map { (emailValid, pwValid) -> emailValid && pwValid }
}
private fun attemptLogin(sEvent: Flowable<Msg>): Flowable<Result> {
return sEvent.map {
when (it) {
is Msg.Email -> Result.Nothing
is Msg.PassWord -> Result.Nothing
is Msg.Submit -> Result.Success
}
}
.distinctUntilChanged()
.filter { it != Result.Nothing }
}
}
}
@Pooh3Mobi
Copy link
Author

Pooh3Mobi commented Jan 14, 2019

Addotional

fun <T : Any> publishProcessor(init: (PublishProcessor<T>.() -> Unit)? = null): PublishProcessor<T> {
    return PublishProcessor.create<T>().apply { if (init != null) init() }
}

fun <T : Any> behaviorProcessor(defV: T? = null, init: (BehaviorProcessor<T>.() -> Unit)? = null): BehaviorProcessor<T> {
    return if (defV != null) {
        BehaviorProcessor.createDefault(defV).apply { if (init != null) init() }
    } else {
        BehaviorProcessor.create<T>().apply { if (init != null) init() }
    }
}

@Suppress("MoveLambdaOutsideParentheses")
inline fun <T, R> update(input: Flowable<T>, total: BehaviorProcessor<R>, crossinline combiner: (T, R) -> R) {
    return input.withLatestFrom(total, { input_, total_ -> combiner(input_, total_) }).subscribe(total)
}

fun <T: Any> Publisher<T>.toLiveData() = LiveDataReactiveStreams.fromPublisher(this)

fun <VDB : ViewDataBinding> LifecycleOwner.bindingWith(binding: VDB, init: VDB.() -> Unit): VDB {
    binding.setLifecycleOwner(this)
    binding.init()
    return binding
}

inline fun <reified VM : ViewModel> Fragment.provideViewModel(): VM = ViewModelProviders.of(this).get(VM::class.java)


interface Inputs
interface Outputs
interface Processor<IN : Inputs, OUT: Outputs> {
    fun create(inputs: IN): OUT
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment