Last active
January 14, 2019 12:16
-
-
Save Pooh3Mobi/2cdb9ec1f6836701ac523c1819eacb95 to your computer and use it in GitHub Desktop.
Login form sample with Functional Reactive Programming + DataBinding by Kotlin + Android
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Addotional