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

[EN] (Unofficial) Guide to App Architecture Gui...

[EN] (Unofficial) Guide to App Architecture Guide Vol 1 - DroidKaigi 2022

Sa-ryong Kang

October 05, 2022
Tweet

More Decks by Sa-ryong Kang

Other Decks in Technology

Transcript

  1. Clean Architecture • There have been long discussion on how

    to adopt Clean Architecture into mobile app design ◦ SOLID principle ◦ Layered component architecture • E.g. Blog post in 2015 5
  2. Clean Architecture is more like a way of thinking than

    a design / architectural pattern. 8
  3. • The important implication from Clean Architecture is that the

    layered structure helps design of mobile application • However, ◦ there’s no specific definition of components in each layer ◦ the diagram in previous page just shows one example of best practices 9 Clean Architecture is not a pattern
  4. Yes, Clean Architecture affected Arch. Guide a lot • Layered

    architecture: UI - Domain - Data Layer • SOLID principle, especially Single Responsibility Principle • However, it's not an implementation of Clean Architecture 11
  5. Why doesn’t the Guide cover these concepts? • MVI (Flux,

    Redux), MVP, … ◦ Not because they are not important • The guide doesn’t intend to cover every patterns and architecture that make sense in mobile design ◦ It’s curated collection of best practices and recommendations for most common use cases 12
  6. Why is “Domain” so important? • It may solve design

    problems caused differences between UI and Data layer. ◦ Even when you don’t need to implement domain “layer”, you need to consider about the mediator • When you focus UI layer too much, ◦ Screen-centric design: it makes difficult deal with logics beyond screen ◦ Agile side effect: elements of screen became backlog / features; then class • When you focus data layer too much, ◦ Data schema affects domain / UI layer directly → typical antipattern 14
  7. Coffee Maker Case Study • A series of coffee makers

    (Mark V will come out eventually) • Warmer plate keeps the pot warm for extended time a. Put coffee grounds into the filter and slide the filter in b. Pour water in the water strainer and press Brew button c. Water is heated until boiling d. The pressure of the steam forces the water to be sprayed over the coffee grounds e. Coffee drips through the filter into the pot 17 Source: https://cleancoders.com/episode/clean-code-episode-15 Image source: https://www.cafeappliances.com/
  8. Coffee Maker Case Study • Boiler (can be turned on

    / off) • Warmer plate (can be turned on / off) • Sensor for the warmer plate (3 states: warmerEmpty, potEmpty, potNotEmpty. • Sensor for the boiler (2 states: boilerEmpty, boilerNotEmpty) • Brew button + Light indicator • A pressure-relief valve 18 Source: https://cleancoders.com/episode/clean-code-episode-15 Image source: https://www.cafeappliances.com/
  9. Think again: is that a screaming architecture? 20 • A

    series of coffee makers (Mark V will come out eventually) • Warmer plate keeps the pot warm for extended time a. Put coffee grounds into the filter and slide the filter in b. Pour water in the water strainer and press Brew button c. Water is heated until boiling d. The pressure of the steam forces the water to be sprayed over the coffee grounds e. Coffee drips through the filter into the pot
  10. Insight • Data- or entity-centric thinking can give you good

    design • Instead, abstracting key actions and actors is more important • You can design mediator like the following: ◦ 1. DDD-ish design ◦ 1-1. Use Case (Transaction Script, which is anti-pattern in DDD) ◦ 2. non-domain mediator layer eg. Gateway, Mapper, Data Controller, Translator, … ◦ 3. put them into Data Layer: Repository 23
  11. Repository pattern • Common misconception: Repository in Android is anti-pattern!?

    ◦ It violates Single Responsibility Principle?! • Repository provides abstraction to be called from domain (or UI) layer ◦ It hides details of data saving / loading 25
  12. Data Source • Data Source hides implementation of data I/O

    • Liskov Substitution Principle (LSP) ◦ When replacing a subclass with another, its behavior shouldn’t be changed ▪ REST ↔ gRPC ▪ Room ↔ other ORM ▪ Real impl. ↔ Fake 26
  13. Considerations on Data Layer • If you only have simple

    operations like CRUD, you may not need both of Repository and Data Source • Consider a separated life cycle in Repository • Who should be the Single Source of Truth in your app? 27
  14. What AAC ViewModel does for you • ACC VM provides

    minimal features to help developers implement general VM ◦ Mechanism to store state safely ▪ Safe from configuration change (by default) ▪ Safe from process kill (thru SavedStateHandler) ◦ Coroutine Scope ◦ Dependency Injection via Dagger Hilt ◦ Helper for Jetpack Navigation 29
  15. What AAC ViewModel does for you • However, ◦ it’s

    completely your job to make VM VM-like ⇒ Adopting AAC VM doesn’t automatically mean you built a good MVVM architecture. ◦ it may not appropriate for some use cases ◦ sometimes implementation of VM seems too verbose 30
  16. If you don’t like it, • You can build your

    own! ◦ And AAC ViewModel source code will inspire you about how to implement Saved State Handler, its own Life Cycle, Coroutines Scope, Navigation, etc. 31
  17. Another Downside of ViewModel: Verbosity • Circular event flow ◦

    (1) View event occurred ◦ (2) ViewModel deal with it and then modify the state ◦ (3) View observes changed state, then show it to the screen 32 override fun onViewCreated(...) { // (1) binding.plusButton.setOnClickListener { _ -> viewModel.incrementCounter() } // (3) viewModel.counter.observe(viewLifeCycleOwner) { binding.plusButton.text = "Count: $it" } } class CounterViewModel { private val _counter = MutableStateFlow(0) val counter: StateFlow get() = _counter.asStateFlow() fun incrementCounter() { // (2) _counter.value += 1 // something aync... } }
  18. Consideration on Jetpack Compose • MVP doesn't make sense in

    many cases ◦ Interaction between View and Presenter is done by method call ◦ Declarative View can’t change the screen by one method call (without help from additional mediator) • ViewModel seems to make more sense, but.. does it? 34
  19. Consideration on Jetpack Compose • Is ViewModel really necessary to

    me? ◦ rememberSavable: the safe state storage provided by AAC ViewModel is now possible in Composable function ◦ If repository in Data layer has independent life cycle from View, ▪ Is it necessary to yield state store to ViewModel? ▪ Some cases it works well without VM (if domain or data layer containes sufficient business logic) 35
  20. Consideration on Jetpack Compose • Be cautious about construction ◦

    What is wrong with the code in the next slide? 36
  21. 37 @Composable fun MyComposable( viewModel: MyViewModel = hiltViewModel() ) {

    Text(text = viewModel.myState) } class MyViewModel : ViewModel() { private val _myState = mutableStateOf("A") val myState: State = _myState init { viewModelScope.launch(Dispatchers.IO) { myState = "B" } } }
  22. Consideration on Jetpack Compose • Be cautious about construction ◦

    1. It changes snapshot state in background scheduler → Crash! ◦ 2. Async task in constructor: The async block in coroutineScope.launch could be finished after the construction was completed → hard to debug when error occurred → make it hard to write test code ◦ 3. Violating Single Responsibility Principle: The main responsibility of constructor is a short and simple instantiation of fields. 38
  23. Consideration on Jetpack Compose • Be cautious about construction ◦

    Suggestions ▪ Implement separated init() method ▪ Or, run initial job when view starts collection / subscription 39
  24. 40 private val _myState = MutableStateFlow("A") val myState: StateFlow =

    _myState.asStateFlow() .onSubscription { // initial loading from local db... } .map { state -> // when _myState updated.. } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), "")
  25. Consideration on Jetpack Compose • Be cautious about the scope

    ◦ Don’t add anything that may affect the snapshot ◦ Don’t store UI state (eg. animation) to VM 41
  26. Consideration on Jetpack Compose • Be cautious about the scope

    ◦ ViewModelScope uses Dispatchers.Main.immediate ▪ instead, you can now use custom CoroutineScope https://developer.android.com/jetpack/androidx/release s/lifecycle#2.5.0 (addClosable(), new constructor of VM) 42
  27. 43 class CloseableCoroutineScope( context: CoroutineContext = SupervisorJob() + Dispatchers.Main )

    : Closeable, CoroutineScope { override val coroutineContext: CoroutineContext = context override fun close() { coroutineContext.cancel() } } class MyViewModel( val customScope: CloseableCoroutineScope = CloseableCoroutineScope() ) : ViewModel(customScope) { // You can now use customScope in the same way as viewModelScope }
  28. Guide to App Architecture Guide Vol. 2 • Dependency Injection

    ◦ Upside and downside of Dagger Hilt ◦ Alternatives • Multimodule ◦ Large scale modular architecture ◦ When is the best timing to separate modules? ◦ Why does my multi module project build so slowly? 44