Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Clean app design with Architecture Components

613633962e5ab27cf572e7699e43d368?s=47 Chuck Greb
November 05, 2017

Clean app design with Architecture Components

Handling lifecycle events, maintaining view state, and persisting data are all common challenges on Android that have contributed to the widespread adoption of clean architecture patterns like Model-View-Presenter (MVP) and Model-View-ViewModel (MVVM).

Android Architecture Components is a new collection of libraries announced at Google I/O to help developers manage these same nagging issues. This talk explores how Architecture Components can be leveraged in an app already using clean architecture principles to help make your code even more flexible, maintainable, and testable.

613633962e5ab27cf572e7699e43d368?s=128

Chuck Greb

November 05, 2017
Tweet

Transcript

  1. Clean App Design with Architecture Components .droidconSF 5 Nov 2017

    Chuck Greb @ecgreb
  2. usebutton.com

  3. None
  4. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model LiveData Storage Room Presenter ViewModel View EditText EditText Button
  5. Clean Architecture

  6. None
  7. None
  8. None
  9. None
  10. None
  11. None
  12. “Elegance in design… is a lack of unnecessary complexity”

  13. Clean Architecture Build layered applications with clear separation of responsibilities

    including view, presentation, and domain logic. Separation of Concerns Synchronize data across screen state, session state, and server state. State Synchronization Enables pure unit tests to test presentation logic and domain models in isolation. Integration and UI tests for components that interact with the framework and/or display. Testability
  14. None
  15. None
  16. None
  17. “Those who do not remember the past
 are condemned to

    repeat it.”
 — George Santayana
  18. Clean Architecture Build layered applications with clear separation of responsibilities

    including view, presentation, and domain logic. Separation of Concerns Synchronize data across screen state, session state, and server state. State Synchronization Enables pure unit tests to test presentation logic and domain models in isolation. Integration and UI tests for components that interact with the framework and/or display. Testability
  19. Separation of Concerns • View layer • Presentation model •

    Domain model
  20. Clean Architecture Build layered applications with clear separation of responsibilities

    including view, presentation, and domain logic. Separation of Concerns Synchronize data across screen state, session state, and server state. State Synchronization Enables pure unit tests to test presentation logic and domain models in isolation. Integration and UI tests for components that interact with the framework and/or display. Testability
  21. State Synchronization • Screen state • Session state • Server

    state
  22. State Synchronization • Screen state • Session state • Server

    state
  23. Clean Architecture Build layered applications with clear separation of responsibilities

    including view, presentation, and domain logic. Separation of Concerns Synchronize data across screen state, session state, and server state. State Synchronization Enables pure unit tests to test presentation logic and domain models in isolation. Integration and UI tests for components that interact with the framework and/or display. Testability
  24. Testability • Android framework dependencies encapsulated in view layer •

    Pure JVM unit tests for presenters and domain models • Espresso, Robolectric, or UI Automator tests for view layer
  25. Test Pyramid

  26. Android Test Pyramid

  27. Integration Tests

  28. Clean Architecture Build layered applications with clear separation of responsibilities

    including view, presentation, and domain logic. Separation of Concerns Synchronize data across screen state, session state, and server state. State Synchronization Enables pure unit tests to test presentation logic and domain models in isolation. Integration and UI tests for components that interact with the framework and/or display. Testability
  29. GUI Architectures

  30. GUI Architectures • MVC • MVP • MVVM • MVI

    • MVWTF • …
  31. None
  32. None
  33. Supervising Controller (MVC) • Smalltalk-80 • One way flow of

    information • Controller handles user input and UI events • View observes changes in domain model State Sync: Data Binding Controller Model onLoginButtonClick() setUser() View View.onClick()
  34. activity_login.xml <data> <variable name="user" type="com.example.loginator.User"/> </data> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}"/>

    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}"/>
  35. Passive View (MVP) • Also called Humble View • No

    dependency between view and domain model • Bi-directional flow of information • View replaced by fake in unit tests State Sync: Flow Synchronization Controller onLoginButtonClick View View.onClick() showUser() Presenter onLoginButtonClick() Model getUser() IView showUser()
  36. class LoginPresenter(private val controller: LoginController) { fun onLoginButtonClick(email: String?, password:

    String?) { if (email != null && password != null) { val user = Loginator.getUser(email, password) if (user != null) { controller.showUser(user) } else { controller.showError("Invalid credentials") } } } } LoginPresenter.kt
  37. Presentation Model (MVVM) • View forwards user input to presentation

    model • Presentation model mutates state using domain model • View observes changes in the presentation model State Sync: Observer Pattern ViewModel Model onLoginButtonClick() getUser() View View.onClick() onUpdateUser() updateUser()
  38. Supervising Controller (MVC) Passive View (MVP) Presentation Model (MVVM) Flow

    of Information One way Two way Two way Model <> View Dependency Yes No No Sync Logic View Presenter View State Synchronization Data binding Flow synchronization Observer pattern GUI Architectures
  39. None
  40. • Passive view migrates all sync logic into presentation layer

    • MVC and MVVM require sync logic in the view layer • For simpler apps, MVC or MVVM may be sufficient • MVP tests are more verbose and require a fake view Humble View is the real MVP
  41. Android Passive View Controller onLoginButtonClick Presenter Activity Presentation Logic Model

    Domain Logic View User Interface Lifecycle Events User Input System Services View
  42. Android Passive View Controller onLoginButtonClick Presenter Activity Presentation Logic Model

    Domain Logic View User Interface Lifecycle Events User Input System Services View
  43. Android Passive View Controller onLoginButtonClick Presenter Activity Presentation Logic Controller

    Model Domain Logic View User Interface Lifecycle Events User Input System Services
  44. Android Passive View onLoginButtonClick Presenter Activity Presentation Logic Controller Model

    Domain Logic View User Interface Lifecycle Events User Input System Services
  45. Controller onLoginButtonClick Presenter Fake Controller Presentation Logic Controller Model Domain

    Logic View User Interface Stub Implementation Presenter Test Model Test Passive View: Unit Tests Test Cases Test Cases
  46. onLoginButtonClick Presenter Activity Presentation Logic Controller Model Domain Logic View

    User Interface Lifecycle Events User Input System Services Passive View: UI Tests Activity Test Test Cases
  47. class LoginActivity : AppCompatActivity(), LoginController { lateinit var presenter: LoginPresenter

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) presenter = LoginPresenter(this) login_button.setOnClickListener({ presenter.onLoginButtonClick(login_view.email, login_view.password) }) } // ... } LoginActivity.kt
  48. interface LoginController { fun showUser(user: User) fun showError(error: String) }

    LoginController.kt
  49. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.loginator.LoginView android:id="@+id/login_view" android:layout_width="match_parent" android:layout_height="match_parent"/> <com.example.loginator.UserView

    android:id="@+id/user_view" android:layout_width="match_parent" android:layout_height="match_parent"/> </FrameLayout> activity_login.xml
  50. class LoginView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr:

    Int = 0) : LinearLayout(context, attrs, defStyleAttr) { var email: String? = null get() = login_email.text.toString() var password: String? = null get() = login_password.text.toString() var error: String? = null set(value) { login_error.text = value } init { (getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater).inflate(R.layout.view_login, this, true) } } LoginView.kt
  51. <EditText android:id="@+id/login_email" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <EditText android:id="@+id/login_password" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:id="@+id/login_button"

    android:layout_width="wrap_content" android:layout_height=“wrap_content"/> <TextView android:id=“@+id/login_error" android:layout_width="wrap_content" android:layout_height="wrap_content"/> view_login.xml
  52. class LoginPresenter(private val controller: LoginController) { fun onLoginButtonClick(email: String?, password:

    String?) { if (email != null && password != null) { val user = Loginator.getUser(email, password) if (user != null) { controller.showUser(user) } else { controller.showError("Invalid credentials") } } } } LoginPresenter.kt
  53. class LoginActivity : AppCompatActivity(), LoginController { // ... override fun

    showUser(user: User) { login_view.visibility = View.GONE user_view.visibility = View.VISIBLE user_view.user = user } override fun showError(error: String) { login_view.error = error } } LoginActivity.kt
  54. class UserView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr:

    Int = 0) : LinearLayout(context, attrs, defStyleAttr) { var user: User? = null set(value) { user_name.text = value?.name user_email.text = value?.email } init { (getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater).inflate(R.layout.view_user, this, true); } } UserView.kt
  55. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/user_name" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView

    android:id="@+id/user_email" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> view_user.xml
  56. Android-y challenges • Foreground/background events • Configuration change • Presenter

    scope • Long-running operations • Data persistence
  57. Android Architecture Components Android Architecture Components

  58. Android Architecture Components • Lifecycle • ViewModel • LiveData •

    Room • Paging Library
  59. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model Model Storage Presenter View EditText EditText Button
  60. Android Architecture Components • Lifecycle • ViewModel • LiveData •

    Room
  61. class LoginActivity : LifecycleActivity(), LoginController { lateinit var presenter: LoginPresenter

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) presenter = LoginPresenter(this) login_button.setOnClickListener({ presenter.onLoginButtonClick(login_view.email, login_view.password) }) } // ... } LoginActivity.kt
  62. class LoginActivity : LifecycleActivity(), LoginController { lateinit var presenter: LoginPresenter

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) presenter = LoginPresenter(this) login_button.setOnClickListener({ presenter.onLoginButtonClick(login_view.email, login_view.password) }) } // ... } LoginActivity.kt
  63. interface LoginController : LifecycleOwner { fun showUser(user: User) fun showError(error:

    String) } LoginController.kt
  64. None
  65. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model Model Storage Presenter View EditText EditText Button
  66. Android Architecture Components • Lifecycle • ViewModel • LiveData •

    Room
  67. class LoginViewModel : ViewModel() { var user: User? = null

    } LoginViewModel.kt
  68. class LoginActivity : LifecycleActivity(), LoginController { lateinit var presenter: LoginPresenter

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) val viewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java) presenter = LoginPresenter(this, viewModel) login_button.setOnClickListener({ presenter.onLoginButtonClick(login_view.email, login_view.password) }) } // ... } LoginActivity.kt
  69. class LoginPresenter(private val controller: LoginController, private val viewModel: LoginViewModel) {

    init { viewModel.user.value?.let { controller.showUser(it) } } fun onLoginButtonClick(email: String?, password: String?) { if (email != null && password != null) { val user = Loginator.getUser(email, password) if (user != null) { controller.showUser(user) viewModel.user = user } else { controller.showError("Invalid credentials") } } } } LoginPresenter.kt
  70. None
  71. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model Model Storage Presenter ViewModel View EditText EditText Button
  72. Android Architecture Components • Lifecycle • ViewModel • LiveData •

    Room
  73. class LoginViewModel : ViewModel() { var user: MutableLiveData<User> = MutableLiveData()

    } LoginViewModel.kt
  74. class LoginPresenter(private val controller: LoginController, private val viewModel: LoginViewModel) {

    init { viewModel.user.observe(controller, Observer<User> { user -> controller.showUser(user) }) } fun onLoginButtonClick(email: String?, password: String?) { if (email != null && password != null) { val user = Loginator.getUser(email, password) if (user != null) { viewModel.user.value = user } else { controller.showError("Invalid credentials") } } } } LoginPresenter.kt
  75. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model LiveData Storage Presenter ViewModel View EditText EditText Button
  76. Android Architecture Components • Lifecycle • ViewModel • LiveData •

    Room
  77. @Entity data class User(@PrimaryKey var id: Int, var name: String,

    var email: String) User.kt
  78. @Dao interface UserDao { @Query("SELECT * FROM user WHERE email

    LIKE :email LIMIT 1") fun getUserByEmail(email: String): LiveData<User> } UserDao.kt
  79. @Dao interface UserDao { @Query("SELECT * FROM user WHERE email

    LIKE :email LIMIT 1") fun getUserByEmail(email: String): LiveData<User> } UserDao.kt
  80. class LoginViewModel : ViewModel() { var user: MutableLiveData<User>? = null

    fun getUser(email: String, password: String): LiveData<User> { if (user == null) { user = Loginator.getUser(email, password) } return user as LiveData<User> } } LoginViewModel.kt
  81. class LoginPresenter(private val controller: LoginController, private val viewModel: LoginViewModel) {

    init { viewModel.user?.observe(controller, Observer<User> { user -> controller.showUser(user) }) } fun onLoginButtonClick(email: String?, password: String?) { if (email != null && password != null) { val user = viewModel.getUser(email, password) if (user.value == null) { controller.showError("Invalid credentials") } } } } LoginPresenter.kt
  82. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model LiveData Storage Room Presenter ViewModel View EditText EditText Button
  83. https://medium.com/android-testing-daily Android Testing https://github.com/ecgreb/loginator Loginator Resources

  84. Some thoughts for the future… • Community-driven architecture • Mix

    and match components • Fragments? • Lifecycle aware libraries • We are all in this together
  85. Fin .droidconSF 5 Nov 2017 Chuck Greb @ecgreb