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

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. Clean App Design with Architecture Components
    .droidconSF
    5 Nov 2017
    Chuck Greb
    @ecgreb

    View Slide

  2. usebutton.com

    View Slide

  3. View Slide

  4. Controller
    LifecycleActivity
    Web
    Service
    Activity Lifecycle
    System Services
    User Input
    Dependency
    Injection
    Repository
    Model
    LiveData
    Storage
    Room
    Presenter
    ViewModel
    View
    EditText
    EditText
    Button

    View Slide

  5. Clean Architecture

    View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. “Elegance in design…
    is a lack of unnecessary complexity”

    View Slide

  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

    View Slide

  14. View Slide

  15. View Slide

  16. View Slide

  17. “Those who do not remember the past

    are condemned to repeat it.”

    — George Santayana

    View Slide

  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

    View Slide

  19. Separation of Concerns
    • View layer
    • Presentation model
    • Domain model

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  25. Test Pyramid

    View Slide

  26. Android Test Pyramid

    View Slide

  27. Integration Tests

    View Slide

  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

    View Slide

  29. GUI Architectures

    View Slide

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

    View Slide

  31. View Slide

  32. View Slide

  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()

    View Slide

  34. activity_login.xml



    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.firstName}"/>
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.lastName}"/>

    View Slide

  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()

    View Slide

  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

    View Slide

  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()

    View Slide

  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

    View Slide

  39. View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  48. interface LoginController {
    fun showUser(user: User)
    fun showError(error: String)
    }
    LoginController.kt

    View Slide

  49. android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    android:id="@+id/login_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    android:id="@+id/user_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    activity_login.xml

    View Slide

  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

    View Slide

  51. android:id="@+id/login_email"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
    android:id="@+id/login_password"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
    android:id="@+id/login_button"
    android:layout_width="wrap_content"
    android:layout_height=“wrap_content"/>
    android:id=“@+id/login_error"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
    view_login.xml

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  55. android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    android:id="@+id/user_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
    android:id="@+id/user_email"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

    view_user.xml

    View Slide

  56. Android-y challenges
    • Foreground/background events
    • Configuration change
    • Presenter scope
    • Long-running operations
    • Data persistence

    View Slide

  57. Android Architecture Components
    Android Architecture Components

    View Slide

  58. Android Architecture Components
    • Lifecycle
    • ViewModel
    • LiveData
    • Room
    • Paging Library

    View Slide

  59. Controller
    LifecycleActivity
    Web
    Service
    Activity Lifecycle
    System Services
    User Input
    Dependency
    Injection
    Repository
    Model
    Model Storage
    Presenter
    View
    EditText
    EditText
    Button

    View Slide

  60. Android Architecture Components
    • Lifecycle
    • ViewModel
    • LiveData
    • Room

    View Slide

  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

    View Slide

  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

    View Slide

  63. interface LoginController : LifecycleOwner {
    fun showUser(user: User)
    fun showError(error: String)
    }
    LoginController.kt

    View Slide

  64. View Slide

  65. Controller
    LifecycleActivity
    Web
    Service
    Activity Lifecycle
    System Services
    User Input
    Dependency
    Injection
    Repository
    Model
    Model Storage
    Presenter
    View
    EditText
    EditText
    Button

    View Slide

  66. Android Architecture Components
    • Lifecycle
    • ViewModel
    • LiveData
    • Room

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  70. View Slide

  71. Controller
    LifecycleActivity
    Web
    Service
    Activity Lifecycle
    System Services
    User Input
    Dependency
    Injection
    Repository
    Model
    Model Storage
    Presenter
    ViewModel
    View
    EditText
    EditText
    Button

    View Slide

  72. Android Architecture Components
    • Lifecycle
    • ViewModel
    • LiveData
    • Room

    View Slide

  73. class LoginViewModel : ViewModel() {
    var user: MutableLiveData = MutableLiveData()
    }
    LoginViewModel.kt

    View Slide

  74. class LoginPresenter(private val controller: LoginController,
    private val viewModel: LoginViewModel) {
    init {
    viewModel.user.observe(controller, Observer {
    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

    View Slide

  75. Controller
    LifecycleActivity
    Web
    Service
    Activity Lifecycle
    System Services
    User Input
    Dependency
    Injection
    Repository
    Model
    LiveData
    Storage
    Presenter
    ViewModel
    View
    EditText
    EditText
    Button

    View Slide

  76. Android Architecture Components
    • Lifecycle
    • ViewModel
    • LiveData
    • Room

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  80. class LoginViewModel : ViewModel() {
    var user: MutableLiveData? = null
    fun getUser(email: String, password: String): LiveData {
    if (user == null) {
    user = Loginator.getUser(email, password)
    }
    return user as LiveData
    }
    }
    LoginViewModel.kt

    View Slide

  81. class LoginPresenter(private val controller: LoginController,
    private val viewModel: LoginViewModel) {
    init {
    viewModel.user?.observe(controller, Observer {
    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

    View Slide

  82. Controller
    LifecycleActivity
    Web
    Service
    Activity Lifecycle
    System Services
    User Input
    Dependency
    Injection
    Repository
    Model
    LiveData
    Storage
    Room
    Presenter
    ViewModel
    View
    EditText
    EditText
    Button

    View Slide

  83. https://medium.com/android-testing-daily
    Android Testing
    https://github.com/ecgreb/loginator
    Loginator
    Resources

    View Slide

  84. Some thoughts for the future…
    • Community-driven architecture
    • Mix and match components
    • Fragments?
    • Lifecycle aware libraries
    • We are all in this together

    View Slide

  85. Fin
    .droidconSF
    5 Nov 2017
    Chuck Greb
    @ecgreb

    View Slide