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

Clean app design with Architecture Components

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.

Chuck Greb

November 05, 2017
Tweet

More Decks by Chuck Greb

Other Decks in Technology

Transcript

  1. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model LiveData Storage Room Presenter ViewModel View EditText EditText Button
  2. 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
  3. “Those who do not remember the past
 are condemned to

    repeat it.”
 — George Santayana
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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()
  10. 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()
  11. 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
  12. 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()
  13. 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
  14. • 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
  15. Android Passive View Controller onLoginButtonClick Presenter Activity Presentation Logic Model

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

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

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

    Domain Logic View User Interface Lifecycle Events User Input System Services
  19. 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
  20. 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
  21. 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
  22. 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
  23. <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
  24. 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
  25. 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
  26. 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
  27. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model Model Storage Presenter View EditText EditText Button
  28. 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
  29. 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
  30. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model Model Storage Presenter View EditText EditText Button
  31. 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
  32. 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
  33. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model Model Storage Presenter ViewModel View EditText EditText Button
  34. 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
  35. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model LiveData Storage Presenter ViewModel View EditText EditText Button
  36. @Dao interface UserDao { @Query("SELECT * FROM user WHERE email

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

    LIKE :email LIMIT 1") fun getUserByEmail(email: String): LiveData<User> } UserDao.kt
  38. 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
  39. 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
  40. Controller LifecycleActivity Web Service Activity Lifecycle System Services User Input

    Dependency Injection Repository Model LiveData Storage Room Presenter ViewModel View EditText EditText Button
  41. Some thoughts for the future… • Community-driven architecture • Mix

    and match components • Fragments? • Lifecycle aware libraries • We are all in this together