The way we build screens on Android changed significantly over the last years. We went from huge classes, strongly coupled with Android and containing a lot of responsibilities to a reactive-based data flow. It helped us to address ever-changing business requirements, extend functionality without adding unmanageable complexity and improve the app’s reliability. In this presentation I talked about lessons learned from this journey and the pros and cons of each approach.
7. ● 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
8. With a lot of features comes a lot of responsibility
9. With a lot of features comes a lot of responsibility
many lines of code
21. Fragment per object type
● Huge classes containing both view and logic
● Deep inheritance hierarchy
● Duplicated functionality
BaseFragment
Fragment
ContactEditFragment
CompanyEditFragment
22. 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);
}
23. 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;
24. Coupling with Android SDK
● No boundary between business logic and platform code
● Handling quirky APIs
● No way to unit test
● Espresso not ready
29. Time went by...
● Building many new screens
● Exploring new patterns, mostly Model-View-Presenter
● New tools, Kotlin, RxJava, Espresso
30. 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
55. 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
61. 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
}
}
}
62. Implementation
sealed class ContactFieldIdentifier {
object Name : ContactFieldIdentifier()
object Phone : ContactFieldIdentifier()
sealed class ContactSectionIdentifier : ContactFieldIdentifier() {
object DefaultSection : ContactSectionIdentifier()
object ContactInformationSection : ContactSectionIdentifier()
}
}
63. 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()
//...
}
81. View effects
● Scroll to a field, show Snackbar, close the screen
StoreView
State
Actions
82. 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)
83. 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)
84. 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)
85. A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
86. A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
87. Data modeling
● Kotlin is a major improvement over Java
Data classes, sealed classes, extension functions, lambdas
88. 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
89. A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
90. 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>
91. A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
93. Mapping data to views
Epoxy
● Abstraction over RecyclerView
● Kotlin support, many add ons
EpoxyTouchHelper
.initSwiping(recyclerView)
.leftAndRight()
.withTarget(MySwippableModel::class.java)
.andCallbacks(swipeCallback)
94. A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
96. Development process
● Significant entry level
● Proficiency in reactive programming
● Duplication and copy-pasting
“Prefer duplication over the wrong abstraction”
Sandi Metz
97. A few more things
● Data modeling
● Separation from Android
● Mapping data to views
● Development process
● Future improvements
98. 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
100. 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