Slide 1

Slide 1 text

Clean App Design with Architecture Components .droidconSF 5 Nov 2017 Chuck Greb @ecgreb

Slide 2

Slide 2 text

usebutton.com

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Clean Architecture

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

“Those who do not remember the past
 are condemned to repeat it.”
 — George Santayana

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

State Synchronization • Screen state • Session state • Server state

Slide 22

Slide 22 text

State Synchronization • Screen state • Session state • Server state

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Test Pyramid

Slide 26

Slide 26 text

Android Test Pyramid

Slide 27

Slide 27 text

Integration Tests

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

GUI Architectures

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

activity_login.xml

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

• 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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

activity_login.xml

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

view_login.xml

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

view_user.xml

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Android Architecture Components Android Architecture Components

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Android Architecture Components • Lifecycle • ViewModel • LiveData • Room

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Android Architecture Components • Lifecycle • ViewModel • LiveData • Room

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

Android Architecture Components • Lifecycle • ViewModel • LiveData • Room

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

Android Architecture Components • Lifecycle • ViewModel • LiveData • Room

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

Fin .droidconSF 5 Nov 2017 Chuck Greb @ecgreb