$30 off During Our Annual Pro Sale. View Details »

Correct UI logic with state machines

Correct UI logic with state machines

Throwing together a screen is easy but as your product grows, business requirements and team members change, maintaining correct functionality in real-life scenarios can be challenging. Test maintenance costs rise with ever more mocks and abstractions. The proposed state machine implementation makes fragile mutable state explicit, easy to understand, to change, and to test.

Presented as a 5 min lightning talk @ App Builders 2019.

Read more at github.com/trafi/states

Avatar for Justas Medeišis

Justas Medeišis

April 30, 2019
Tweet

More Decks by Justas Medeišis

Other Decks in Programming

Transcript

  1. After entering their phone number, the user receives an SMS

    with a code. In this screen they manually enter the 4-digit code. If possible,
  2. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If
  3. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown.
  4. private fun beginVerification(isFirstVerification: Boolean = false) { countDownTimer = object

    : CountDownTimer(10000, 1000) { override fun onTick(millisUntilFinished: Long) { resend_button.text = getString(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_RESEND_TEXT_WITH_TIME, (millisUntilFinished / 1000).toString()) } override fun onFinish() { resend_button.text = getString(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_RESEND_TEXT) resend_button.isEnabled = true } }.also { it.start() } resend_button.isEnabled = false progressModal.show(childFragmentManager) accountService.verifyPhone(VerifyPhoneNumberParameters( phoneNumber = phoneNumber, template = messageTemplateStart + messageTemplateEnding )).apply { enqueue(callback( onSuccess = { userStore.user = it if (isForeground()) { progressModal.dismiss() if (!isFirstVerification) { snack_bar_target.showSnackBar(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_TOAST_MESSAGE_SENT) } code_input.showKeyboard() } }, onError = { if (isForeground()) { progressModal.dismiss() countDownTimer?.cancel() countDownTimer?.onFinish() ErrorModal.newInstance(context, it).show(childFragmentManager, if (isFirstVerification) { BEGIN_FIRST_VERIFICATION_MODAL_TAG } else { BEGIN_VERIFICATION_MODAL_TAG }) } } )) } } override fun onPrimaryButtonClick(tag: String) { when (tag) { BEGIN_FIRST_VERIFICATION_MODAL_TAG -> navigator.navigateBack() } } Code with good intentions
  5. private fun beginVerification(isFirstVerification: Boolean = false) { countDownTimer = object

    : CountDownTimer(10000, 1000) { override fun onTick(millisUntilFinished: Long) { resend_button.text = getString(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_RESEND_TEXT_WITH_TIME, (millisUntilFinished / 1000).toString()) } override fun onFinish() { resend_button.text = getString(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_RESEND_TEXT) resend_button.isEnabled = true } }.also { it.start() } resend_button.isEnabled = false progressModal.show(childFragmentManager) accountService.verifyPhone(VerifyPhoneNumberParameters( phoneNumber = phoneNumber, template = messageTemplateStart + messageTemplateEnding )).apply { enqueue(callback( onSuccess = { userStore.user = it if (isForeground()) { progressModal.dismiss() if (!isFirstVerification) { snack_bar_target.showSnackBar(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_TOAST_MESSAGE_SENT) } code_input.showKeyboard() } }, onError = { if (isForeground()) { progressModal.dismiss() countDownTimer?.cancel() countDownTimer?.onFinish() ErrorModal.newInstance(context, it).show(childFragmentManager, if (isFirstVerification) { BEGIN_FIRST_VERIFICATION_MODAL_TAG } else { BEGIN_VERIFICATION_MODAL_TAG }) } } )) } } override fun onPrimaryButtonClick(tag: String) { when (tag) { BEGIN_FIRST_VERIFICATION_MODAL_TAG -> navigator.navigateBack() } }
  6. ?

  7. private fun beginVerification(isFirstVerification: Boolean = false) { countDownTimer = object

    : CountDownTimer(10000, 1000) { override fun onTick(millisUntilFinished: Long) { resend_button.text = getString(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_RESEND_TEXT_WITH_TIME, (millisUntilFinished / 1000).toString()) } override fun onFinish() { resend_button.text = getString(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_RESEND_TEXT) resend_button.isEnabled = true } }.also { it.start() } resend_button.isEnabled = false progressModal.show(childFragmentManager) accountService.verifyPhone(VerifyPhoneNumberParameters( phoneNumber = phoneNumber, template = messageTemplateStart + messageTemplateEnding )).apply { enqueue(callback( onSuccess = { userStore.user = it if (isForeground()) { progressModal.dismiss() if (!isFirstVerification) { snack_bar_target.showSnackBar(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_TOAST_MESSAGE_SENT) } code_input.showKeyboard() } }, onError = { if (isForeground()) { progressModal.dismiss() countDownTimer?.cancel() countDownTimer?.onFinish() ErrorModal.newInstance(context, it).show(childFragmentManager, if (isFirstVerification) { BEGIN_FIRST_VERIFICATION_MODAL_TAG } else { BEGIN_VERIFICATION_MODAL_TAG }) } } )) } } override fun onPrimaryButtonClick(tag: String) { when (tag) { BEGIN_FIRST_VERIFICATION_MODAL_TAG -> navigator.navigateBack() } } Code with good intentions
  8. After entering their phone number, the user receives an SMS

    with a code. In this screen they manually enter the 4-digit code. If possible,
  9. After entering their phone number, the user receives an SMS

    with a code. In this screen they manually enter the 4-digit code. If possible,
  10. After entering their phone number, the user receives an SMS

    with a code. In this screen they manually enter the 4-digit code. If possible, codeEntered(code)
  11. After entering their phone number, the user receives an SMS

    with a code. In this screen they manually enter the 4-digit code. If possible, c
  12. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If c
  13. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If c
  14. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If c smsReceived(msg)
  15. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If c s
  16. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown. c s
  17. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown. c s
  18. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown. codeVerified c s
  19. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown. codeVerified codeRejected c s
  20. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If
  21. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If
  22. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If checkCode(code)
  23. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If c
  24. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If c
  25. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If c showProgress
  26. the 4-digit code. If possible, the code is automatically prefilled.

    The code is checked immediately as soon as 4 digits have been entered. The user cannot edit the code while it is being checked. If c s
  27. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown. c s
  28. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown. c s
  29. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown. c s showError(msg)
  30. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown. c s s
  31. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown. c s s
  32. while it is being checked. If the correct code is

    entered, the phone number is verified and the screen closes. Otherwise, an error dialog is shown. c s s close
  33. private fun beginVerification(isFirstVerification: Boolean = false) { countDownTimer = object

    : CountDownTimer(10000, 1000) { override fun onTick(millisUntilFinished: Long) { resend_button.text = getString(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_RESEND_TEXT_WITH_TIME, (millisUntilFinished / 1000).toString()) } override fun onFinish() { resend_button.text = getString(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_RESEND_TEXT) resend_button.isEnabled = true } }.also { it.start() } resend_button.isEnabled = false progressModal.show(childFragmentManager) accountService.verifyPhone(VerifyPhoneNumberParameters( phoneNumber = phoneNumber, template = messageTemplateStart + messageTemplateEnding )).apply { enqueue(callback( onSuccess = { userStore.user = it if (isForeground()) { progressModal.dismiss() if (!isFirstVerification) { snack_bar_target.showSnackBar(R.string.ACCOUNTS_EDIT_PROFILE_ADD_NUMBER_STEP2_TOAST_MESSAGE_SENT) } code_input.showKeyboard() } }, onError = { if (isForeground()) { progressModal.dismiss() countDownTimer?.cancel() countDownTimer?.onFinish() ErrorModal.newInstance(context, it).show(childFragmentManager, if (isFirstVerification) { BEGIN_FIRST_VERIFICATION_MODAL_TAG } else { BEGIN_VERIFICATION_MODAL_TAG }) } } )) } } override fun onPrimaryButtonClick(tag: String) { when (tag) { BEGIN_FIRST_VERIFICATION_MODAL_TAG -> navigator.navigateBack() } } Code with good intentions
  34. ?

  35. ?

  36. ?

  37. ?

  38. { enum Event {/* */} var checkCode: Code? var showProgress:

    Bool var showError: Error? var close: Bool? }
  39. struct PhoneVerifState { enum Event {/* */} var checkCode: Code?

    var showProgress: Bool var showError: Error? var close: Bool? }
  40. struct PhoneVerifState { enum Event {/* */} var checkCode: Code?

    var showProgress: Bool var showError: Error? var close: Bool? }
  41. fun reduce(e: Event) = when (e) { // .. is

    smsReceived -> { e.msg.parseCode() ?.let { checkCode(it) } ?: copy() } // .. }
  42. fun reduce(e: Event) = when (e) { // .. codeRejected

    -> copy( showError = Error("Code rej.")) // .. }
  43. If they get the code wrong, the user can tap

    on resend to send a new SMS.
  44. If they get the code wrong, the user can tap

    on resend to send a new SMS.
  45. enum Event { case codeEntered(code: String) case smsReceived(msg: String) case

    codeVerified case codeRejected case resendTapped }
  46. struct PhoneVerifState { var checkCode: Code? var sendSms: String? var

    showProgress: Bool var showError: Error? var close: Bool? }
  47. But, we must not let users send SMS more than

    once every 10s to avoid abuse.
  48. But, we must not let users send SMS more than

    once every 10s to avoid abuse.
  49. enum Event { case codeEntered(code: String) case smsReceived(msg: String) case

    codeVerified case codeRejected case resendTapped }
  50. enum Event { case codeEntered(code: String) case smsReceived(msg: String) case

    codeVerified case codeRejected case resendTapped case secondPassed }
  51. struct PhoneVerifState { var checkCode: Code? var sendSms: String? var

    showProgress: Bool var showError: Error? var close: Bool? }
  52. struct PhoneVerifState { var checkCode: Code? var sendSms: String? var

    showProgress: Bool var showError: Error? var close: Bool? }
  53. struct PhoneVerifState { var checkCode: Code? var sendSms: String? var

    showProgress: Bool var showError: Error? var resendEnabled: Bool var close: Bool? }
  54. fun reduce(e: Event) = when (e) { // .. secondPassed

    -> copy( waitSeconds = maxOf(0, waitSeconds - 1)) // .. }
  55. fun reduce(e: Event) = when (e) { // .. resendTapped

    -> copy( sendSms = true, waitSeconds = 10) // .. }
  56. sealed class Event { class codeEntered(val code: Strin class smsReceived(val

    msg: String object codeVerified: Event() object codeRejected: Event() }