Building complex
screens on
Android
Maciej Witowski
April 10th 2019, 1pm
Usability
#ZES19
Let’s get started!
Check-in to the session
on the App!
Turn on the Zoom! Record the Zoom!
#ZES19
A long time ago...
A long time ago...
A long time ago...
● One of the top mobile CRMs
in App Store and Play Store
● 8000 paying users
● Offline mode, Voice, Calendar, Maps etc.
● Average screen time at 71 minutes per day
With a lot of features comes a lot of responsibility
With a lot of features comes a lot of responsibility
many lines of code
> 400k LOC
This talk
Old vs New way of building screens
Main types of objects in Sell
Main types of objects in Sell
Contact
Company
Person
Lead
Deal
Main types of objects in Sell
Contact
Company
Person
Lead
Deal
Main types of objects in Sell
Contact
Company
Person
Lead
Deal
You can create new
objects and edit
existing ones
Our task
Build the edit screens for these objects
The Old Way
Types Edit Screens
Contact
Company
Person
Lead
Deal
Types Edit Screens
Contact
Company
Person
Lead
Deal
ContactEditFragment
CompanyEditFragment
PersonEditFragment
DealEditFragment
LeadEditFragment
Fragment per object type
● Huge classes containing both view and logic
● Deep inheritance hierarchy
● Duplicated functionality
BaseFragment
Fragment
ContactEditFragment
CompanyEditFragment
Callbacks everywhere
● No clear direction
● Confusing lifecycle
● Problematic events handling
● Errors propagation
@OnClick(R.id.show_company)
private void onClick() {
Loader<Company> loader = new Loader<Company>(){
@Override
public void onLoadFinished(Company company) {
setCompanyData(company);
}
};
initLoader(loader);
}
Mutable state
● Extreme complexity growth
● Hard to debug
● Concurrency problem
private ContactData initialContactData;
private ContactData currentContactData;
private ContactData temporaryContactData;
private boolean isErrorShown;
private boolean canSaveNow;
Coupling with Android SDK
● No boundary between business logic and platform code
● Handling quirky APIs
● No way to unit test
● Espresso not ready
Modeling with primitives
● No built-in support for value objects in Java
● Working with Android SDK
Modeling with primitives
● No built-in support for value objects in Java
● Working with Android SDK
● “Performance impact”
Old times - sum up
● Inheritance
● Callbacks
● Mutable state
● Coupling with the Android SDK
● Lack of tests
● Modeling the logic using primitives
Time went by...
Time went by...
● Building many new screens
● Exploring new patterns, mostly Model-View-Presenter
● New tools, Kotlin, RxJava, Espresso
Time went by...
● Building many new screens
● Exploring new patterns, mostly Model-View-Presenter
● New tools: Kotlin, RxJava, Espresso
When it comes to the edit screens
● Small product changes
● Didn’t justify the investment
● Sticking the new code and hoping it will work
2017: Required Fields
What is a better way to build screens?
Unidirectional Data Flow
StoreView
State
Actions
Unidirectional Data Flow
StoreView
State
https://github.com/zendesk/Suas-Android
Actions
Unidirectional Data Flow
View Reducer
Fields Builder
Configuration
Provider
Store
Unidirectional Data Flow
Unidirectional Data Flow
name: null,
phone: null
Unidirectional Data Flow
name: null,
phone: null
name: null,
phone: null
Unidirectional Data Flow
name: null,
phone: null
field(name: null),
field(phone: null)
name: null,
phone: null
Unidirectional Data Flow
name: null,
phone: null
field(name: null),
field(phone: null)
name: null,
phone: null
field(name: null),
field(phone: null)
Unidirectional Data Flow
field(name: null),
field(phone: null)
name: null,
phone: null
field(name: null),
field(phone: null)
name: null,
phone: null
Unidirectional Data Flow
field(name: null),
field(phone: null)
name: null,
phone: null
field(name: null),
field(phone: null)
name: null,
phone: null
name: “Joe”
Unidirectional Data Flow
field(name: null),
field(phone: null)
name: Joe,
phone: null
field(name: null),
field(phone: null)
name: null,
phone: null
Unidirectional Data Flow
field(name: null),
field(phone: null)
name: Joe,
phone: null
name: null,
phone: null
field(name: Joe),
field(phone: null)
Unidirectional Data Flow
field(name: null),
field(phone: null)
name: Joe,
phone: null
name: null,
phone: null
field(name: Joe),
field(phone: null)
field(name: Joe),
field(phone: null)
Unidirectional Data Flow
field(name: “Joe”),
field(phone: null)
name: Joe,
phone: null
name: null,
phone: null
field(name: Joe),
field(phone: null)
Unidirectional Data Flow
field(name: “Joe”),
field(phone: null)
name: Joe,
phone: null
name: null,
phone: 555
field(name: Joe),
field(phone: null)
Unidirectional Data Flow
field(name: “Joe”),
field(phone: null)
name: Joe,
phone: 555
name: null,
phone: 555
field(name: Joe),
field(phone: null)
Unidirectional Data Flow
field(name: “Joe”),
field(phone: null)
name: Joe,
phone: 555
name: null,
phone: 555
field(name: Joe),
field(phone: 555)
Unidirectional Data Flow
field(name: “Joe”),
field(phone: null)
name: Joe,
phone: 555
name: null,
phone: 555
field(name: Joe),
field(phone: 555)
field(name: Joe),
field(phone: 555)
Unidirectional Data Flow
field(name: “Joe”),
field(phone: “555”)
name: Joe,
phone: 555
name: null,
phone: 555
field(name: Joe),
field(phone: 555)
Is that it?
Is that it?
● Database changes
● Handling conflicts
● Different field types
● Sections, ordering
● Filtering
● Navigation
● Prefilled values
● Validations
● Persisting
● Confirmations, discarding changes
● ...
Configuration
Provider Reducer
Fields Builder
View
Store - key concepts
● Directed acyclic graph
● Single Store and Reducer implementations
● Unit tested the most
● Composition of simple dependencies unique per business type
● Dagger as a glue
Implementation
Implementation
class Store<FieldIdentifier>(
private val reducer: Reducer<FieldIdentifier>,
private val layoutProcessor: LayoutProcessor<FieldIdentifier>,
private val fieldsBuilder: FieldsBuilder<FieldIdentifier>,
//…
) {
fun start(initialState: State<FieldIdentifier>) {
//…
}
fun stop() {
//...
}
}
Implementation
class Store<FieldIdentifier>(
private val reducer: Reducer<FieldIdentifier>,
private val layoutProcessor: LayoutProcessor<FieldIdentifier>,
private val fieldsBuilder: FieldsBuilder<FieldIdentifier>,
//…
) {
fun start(initialState: State<FieldIdentifier>) {
//…
}
fun stop() {
//...
}
}
Implementation
class Store<FieldIdentifier>(
private val reducer: Reducer<FieldIdentifier>,
private val layoutProcessor: LayoutProcessor<FieldIdentifier>,
private val fieldsBuilder: FieldsBuilder<FieldIdentifier>,
//…
) {
fun start(initialState: State<FieldIdentifier>) {
//…
}
fun stop() {
//...
}
}
Implementation
class Reducer<FieldIdentifier> {
fun reduce(
fields: Observable<Set<FieldIdentifier>>,
userUpdates: Observable<Set<FieldIdentifier>>,
//...
): Observable<State<FieldIdentifier>>
}
Implementation
class Reducer<FieldIdentifier> {
fun reduce(
fields: Observable<Set<FieldIdentifier>>,
userUpdates: Observable<Set<FieldIdentifier>>,
//...
): Observable<State<FieldIdentifier>> {
return Observable
.merge(fields, userUpdated /* ... */)
.scan(initialState) { state, event ->
// Build new state
}
}
}
Implementation
sealed class ContactFieldIdentifier {
object Name : ContactFieldIdentifier()
object Phone : ContactFieldIdentifier()
sealed class ContactSectionIdentifier : ContactFieldIdentifier() {
object DefaultSection : ContactSectionIdentifier()
object ContactInformationSection : ContactSectionIdentifier()
}
}
Implementation
class ContactConfigurationProvider : ConfigurationProvider<ContactFieldIdentifier> {
fun getFields(): Observable<Set<ContactFieldIdentifier>>
//...
}
}
class ContactFieldValuesProvider : FieldValuesProvider<ContactFieldIdentifier> {
fun getFieldValues(fields: Set<ContactFieldIdentifier>):
Observable<Map<ContactFieldIdentifier, Value?>> {
//...
}
}
sealed class Value {
data class LongValue(val value: Long) : Value()
data class StringValue(val value: String) : Value()
//...
}
The graph
The graph - challenges
● Responsibilities
● Invalidations
● Side effects
● Unexpected requirements
The graph - challenges
● Clear flow
● Threading
● Debugging
● Extensibility
Cycles
View
Reducer
Configuration
Provider
Store
Fields Builder
View
Reducer
Configuration
Provider
Store
Display Values
Provider
Fields Builder
View
name: Joe,
company: 1
Store
View
name: Joe,
company: 1
name: Joe,
company: 1
Store
View
name: Joe,
company: 1
name: Joe,
company: 1
Store
name: Joe,
company: VW
View
name: Joe,
company: 1
name: Joe,
company: 1
Store
name: Joe,
company: VW
field(name: Joe),
field(company: {1, VW})
View
name: Joe,
company: 1
name: Joe,
company: 1
Store
name: Joe,
company: VW
field(name: Joe),
field(company: {1, VW})
View
name: Joe,
company: 1
name: Joe,
company: 1
Store
name: Joe,
company: VW
field(name: Joe),
field(company: 1)
Fallback:
company: null
View
name: Joe,
company: null
name: Joe,
company: 1
Store
name: Joe,
company: VW
field(name: Joe),
field(company: 1)
Fallback:
company: null
View
name: Joe,
company: null
name: Joe,
company: 1
Store
name: Joe,
company: null
field(name: Joe),
field(company: null)
Fallback:
company: null
View
name: Joe,
company: null
name: Joe,
company: 1
Store
name: Joe,
company: null
field(name: Joe),
field(company: null)
Fallback:
company: null
Cycles
● Limit or remove
● Rethink early
● Watch out for Rx Subjects
View effects
View effects
● Scroll to a field, show Snackbar, close the screen
StoreView
State
Actions
View effects
● Scroll to a field, show Snackbar, close the screen
● Option 1: Pass them in State
StoreView
State
Actions
field(name: Joe),
effect(scroll to top)
View effects
● Scroll to a field, show Snackbar, close the screen
● Option 1: Pass them in State
StoreView
State
Actions
field(name: Joe),
effect(null)
View effects
● Scroll to a field, show Snackbar, close the screen
● Option 1: Pass them in State
● Option 2: Separate them
StoreView
State
Actions
effect(scroll to top)
A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
Data modeling
● Kotlin is a major improvement over Java
Data classes, sealed classes, extension functions, lambdas
Data modeling
● Kotlin is a major improvement over Java
Data classes, sealed classes, extension functions, lambdas
● Android Runtime improvements
“Creating garbage is OK. Use the types and objects you need.”
Nicolas Geoffray, Android Runtime Team
A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
Separation from Android
● Build wrapper classes with interfaces and clear API
● Abstraction increase
● Designing an API will be on you!
fun requestPermissions(
usage: RuntimePermissionsUsage
): Observable<RuntimePermissionsResult>
A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
Mapping data to views
Mapping data to views
Epoxy
● Abstraction over RecyclerView
● Kotlin support, many add ons
EpoxyTouchHelper
.initSwiping(recyclerView)
.leftAndRight()
.withTarget(MySwippableModel::class.java)
.andCallbacks(swipeCallback)
A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
Development process
● Significant entry level
● Proficiency in reactive programming
Development process
● Significant entry level
● Proficiency in reactive programming
● Duplication and copy-pasting
“Prefer duplication over the wrong abstraction”
Sandi Metz
A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
Future improvements
● Splitting the Store (~200 LOC)
● Reevaluating existing cyclic dependencies
● More type-safe modelling
● Developer experience
○ Initial setup
○ Unified naming
○ Tracking or rewinding state
Conclusion
Conclusion
Old New
Inheritance Composition
Callbacks RxJava-based blocks
Mutable state Unidirectional flow
Modeling with primitives Higher level abstractions with Kotlin
No separation from Android SDK Android SDK kept in isolated blocks
Lack of tests
Unit tests for the core logic, Espresso
for integrations
Questions?
Thanks!
Don’t Forget!
Fill out the Session
Survey in the App
End Zoom Recording
Leave the room better
than you found it!
#ZES19
#ZES19

Building complex UI on Android