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

AppDevCon 2022 - Compose Yourself!

AppDevCon 2022 - Compose Yourself!

Slide deck for the AppDevCon 2022 Compose Yourself! Workshop together with Paul Lammertsma

81dddfb847c5224ba9bd0064131ae59a?s=128

Iulia STANA

June 24, 2022
Tweet

Other Decks in Education

Transcript

  1. Compose Yourself! Part 1 - AppDevCon Workshop 2022

  2. Jetpack Compose Declarative UI toolkit Written fully in Kotlin Unbundled

    (minSDK 21) 2
  3. Goals for Part 1 ❏ Use composable functions to describe

    a Compose UI ❏ Define what composition & recomposition are ❏ List the ways recomposition execution impacts your Compose code 3
  4. Repository structure 1. Checkout code from: https://github.com/IuliaSTANA/composeAppDevCon 2. Each individual

    folder is a project 3. In each Part folder there is a begin and complete project 4
  5. Part 1 Worksheet 1. Go through the Questions in the

    Part01/README.md 2. Try to code along for questions 6 through 8 5
  6. Section Y: Theory ❏ Building Compose UI ❏ Updating Compose

    UI ❏ Define what composition & recomposition are ❏ List the ways recomposition execution impacts your Compose code 6
  7. Building UI class MainActivity : AppCompatActivity() { public override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) findViewById<TextView>(R.id.greeting).text = "Hello" } } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text(text = "Hello") } } } 7
  8. Building UI XML • UI Widgets are instantiated in code

    or inflated from XML layouts • Widgets are mutable, can be queried and modified • Attach to parent… ? Compose • UI is described, not instantiated • UI is immutable and cannot be queried • UI widgets are stateless • Compose Compiler builds UI from running composables = The Composition • Composition is a tree-structure of the composables describing the UI 8
  9. Building UI The Composition: the UI built by Compose when

    it executes composable functions Initial composition: creation of a Composition by running composables the first time 9 Row Image Column Text Text
  10. Updating UI fun bind(plants: LiveData<List<Plant>>) { plants.observe(this) { it-> updatePlantsInventory(it)

    } } @Composable fun PlantOverviewScreen(plants: LiveData<List<Plant>>){ val plantsOverview by plants.observeAsState() PlantOverview(inventory = plantsOverview) } //Lifecycle? 10
  11. Updating UI XML • Widgets have state • State exists

    also in activity/fragment • Updating the UI requires synchronizing the 2 states: ◦ findViewById() ◦ Change view state or ◦ Data binding Compose: • Will keep track of the composables describing the UI during the initial composition • A state change results in a recomposition being scheduled • Recomposition: re-execution of composables that may have changed in response to state changes • Composition is updated to reflect any changes 11
  12. Compose UI A composition can only be produced by an

    initial composition and updated by recomposition. The only way to modify a composition is through recomposition. 12
  13. Recomposing the entire UI tree can be expensive. To solve

    this problem Compose uses intelligent recomposition. When Compose recomposes due to new input it will: • Only call the functions that might have changed • Skip the rest of the functions Rules & Regulations for Recomposition What to be aware of when writing Compose code 13
  14. Rules & Regulations for Recomposition What to be aware of

    when writing Compose code 1. Composable functions can execute in any order 2. Composable functions can execute in parallel 3. Recomposition skips as many composable functions and lambdas as possible 4. Recomposition is optimistic and may be canceled 5. A composable function might be run quite frequently, as often as every frame of an animation 14
  15. 1 - Composable functions can execute in any order @Composable

    fun ButtonRow() { MyFancyNavigation { TCScreen() SecondScreen() EndScreen() } } Compose can recognize that some composable functions are higher priority than others, thus drawing them first The calls to screens might happen in any order: the order in code does not guarantee the same order of execution. Each function needs to be self contained. 15
  16. 1 - Composable order execution: Bad example @Composable fun ButtonRow()

    { MyFancyNavigation { var haveShownTC = … //From ViewModel TCScreen { haveShownTC = true } SecondScreen(haveShownTC) EndScreen() } } @Composable fun TCScreen(didShowTC: () -> Unit) {... didShowTC() } 16
  17. 1 - Composable order execution: Correct @Composable fun ButtonRow() {

    MyFancyNavigation { var didViewTC = … //From ViewModel TCScreen(didViewTC) { didViewTC = true } SecondScreen(didViewTC) EndScreen() } } @Composable fun TCScreen(didViewTC, onViewTC: (Boolean) -> Unit) { Checkbox(checked = didViewTC, onCheckedChange = { onViewTC(it) } } 17
  18. 2 - Composable functions can execute in parallel @Composable fun

    ListComposable(myList: List<String>) { Column { for (item in myList) { Text("Item: $item") } } Text("Count: ${myList.size}") } Compose can optimize recomposition by running composable functions in parallel. Composable functions might execute on a pool of background threads. Trigger side-effects from callbacks (i.e. onClick) which are guaranteed to run on main thread. 18
  19. 2 - Composable functions can execute in parallel Bad example

    @Composable fun ListWithBug(myList: List<String>) { var items = 0 Column { for (item in myList) { Text("Item: $item") items++ } } Text("Count: $items") } 19
  20. 2 - Composable functions can execute in parallel Bad example:

    why? @Composable fun ListWithBug(myList: List<String>) { var items = 0 Column { for (item in myList) { Text("Item: $item") items++ } } Text("Count: $items") } Not thread safe Forbidden side effects Composables should be side-effects free 20
  21. @Composable fun SkippableComposable(first:String, second: Boolean, third: String) = Column {

    Text(text = first) Checkbox(checked = second, onCheckedChange = {}) Text(text = third) } 3 - Recomposition skips as many composable functions and lambdas as possible Any one of the Column elements may be recomposed independently of the other 2, if only its corresponding state is what changed. Compose does not need to recompose the full path in the UI tree if nothing has changed. When you need to perform a side-effect, trigger it from a callback. 21
  22. 4 - Recomposition is optimistic and may be canceled Compose

    expects to finish recomposition before the parameters change again. If a parameter does change before recomposition finishes, Compose might cancel the recomposition and restart it. When recomposition is canceled, Compose discards the UI tree from the recomposition. If you have any side-effects that depend on the UI being displayed, the side-effect will be applied even if composition is canceled resulting in inconsistent app state. Composable functions should be “idempotent” 22
  23. 5 - A composable function might be run quite frequently,

    as often as every frame of an animation Don’t perform expensive operations (ex: reading from local storage) inside a composable If a composable function needs data, you should define it as a parameter Move expensive work outside of the composable and pass the data using mutableStateOf 23
  24. Resources & further reading 1. Thinking in Compose | Jetpack

    Compose | Android Developers 2. State and Jetpack Compose | Android Developers 3. Lifecycle of composables | Jetpack Compose | Android Developers 4. Jetpack Compose | Android Developers 24
  25. End of Part 1 Thanks to reviewers: Sara Hachem, Oya

    Canli, Hugo Visser, Code highlighting for slides: https://romannurik.github.io/SlidesCodeHighlighter App Icons: https://thenounproject.com/icon/plant-4679181/ App Idea: https://www.pickuplimes.com/ 25
  26. Compose Yourself! Part 2 - Manage State 26

  27. Completed Part 1 ❏ Building Compose UI ❏ Updating Compose

    UI ❏ Define what composition & recomposition are ❏ List the ways recomposition execution impacts your Compose code 27
  28. Goals for Part 2 ❏ Use Material 3 Theming and

    Navigation ❏ Manage state in your composable functions ❏ Define state hoisting and unidirectional data flow ❏ List guidelines for managing state 28
  29. Part 2 Worksheet 1. Go through the Questions in the

    Part02/README.md 2. Try to find the answers for the questions in README.md 29
  30. State in Compose - Coda • State & MutableState are

    the observable types in compose • Any changes to a State value will schedule a recomposition of any composable functions that read that value 30
  31. State API State converters available for common observable types: //LiveData

    viewModel.name.observeAsState("") //Flow viewModel.name.collectAsState("") //Rx2 viewModel.name.subscribeAsState("") 31
  32. Remember State State created in composables needs to be remembered

    Use remember/rememberSaveable to add the state to the Compose UI tree and reuse the state value between recompositions 32
  33. Available remember functions 1. remember functions work with Bundle. You

    can create your own functions or pass in a custom saver 2. Material components have their own remember functions (ex: rememberTopAppBarScrollState,rememberScaffoldState, etc) for storing their UI state 3. Use remember for transient state (ex: animation) and rememberSaveable for state that needs to be reused across activity and process recreation. 33
  34. Remember for coroutine Scope rememberCoroutineScope Creates a coroutine scope bound

    to this point in the composition. Use this scope to launch jobs in response to callback events where the response to that event needs to unfold over time and be cancelled if the composable managing that process leaves the composition. 34
  35. Callbacks You should only mutate state outside of a composable

    function 35
  36. Callbacks val inputText = rememberSaveable { mutableStateOf("") } OutlinedTextField( value

    = inputText.value, onValueChange = { inputText.value = it }, label = { Text(“Add Plant”) } ) 36
  37. Callbacks val inputText = rememberSaveable { mutableStateOf("") } OutlinedTextField( value

    = inputText.value, onValueChange = { inputText.value = it }, label = { Text(text = “Add Plant”) } ) 37
  38. Callbacks Callbacks are outside of the Composable scope Callbacks triggered

    by user input/interaction (ex: onClick, onValueChange etc) are guaranteed to be called on the main UI thread. 38
  39. Kotlin Property Delegation in State API State API supports kotlin

    property delegation. The 3 declarations are equivalent: val mutableState = remember { mutableStateOf(default) } var value by remember { mutableStateOf(default) } val (value, setValue) = remember { mutableStateOf(default) } 39
  40. Stateful vs. Stateless Composables that create and change their own

    state are stateful composables. + The caller does not need to control the state and can use the stateful composable without having to manage the state themselves - Stateful composables are less reusable - Stateful composables are harder to test 40
  41. How? hoist: intransitive verb To raise or haul up, often

    with the help of a mechanical apparatus. Synonym: lift 41
  42. From Stateful to Stateless with State Hoisting State hoisting: pattern

    of moving state to a composables caller to make the called composable stateless 42
  43. State Hoisting The internal state variable is replaced with 2

    parameters(*): //Current value to display value: T //event that requests the value to change, T being the new value onValueChange: (T)->Unit (*) YMMV: use more specific event handlers as required. 43
  44. Properties of Hoisted State • Single source of truth state

    is moved, not duplicated • Encapsulated state is internal to its composable, cannot be modified outside of it • Shareable/reusable • Interceptable callers to stateless composables can decide to ignore or modify events before changing the state (ex: input validation) • Decoupled the state can be stored anywhere 44
  45. State Hoisting for Screen @Composable fun AddEditPlantScreen( plantId: String?, onPlantUpdate:

    () -> Unit, viewModel: AddEditPlantViewModel, state: AddEditPlantState = rememberAddEditPlantState( plantId = plantId, //… 45
  46. State Hoisting for Content val currentPlant by viewModel.currentPlant.observeAsState(Plant()) AddEditPlantContent( name

    = currentPlant.name,//.. isError = state.isNameInvalid, onNameChange = state::onNameChange, validateName = state::validateName,//… 46
  47. Unidirectional Data Flow Decouple composables that display state in the

    UI from the parts in the app that store and change state Composables should receive state as parameters and communicate events up using lambdas AddEditPlantScreen AddEditPlantContent state event 47
  48. Guidelines for state hoisting Goldilocks zone: don’t move the state

    higher or lower than you need it for Birds of a feather stick together: If two states change in response to the same event they should be hoisted together Code smells to look out for: unused parameters, too long parameter list • Only pass in the arguments the composable needs • Your composables can have other composables as parameters Write Previews: having previews for your composable is a good litmus test for whether your function is self-contained. 48
  49. Too long parameter list Rule of thumb: ~7 parameters should

    be easy to remember Kotlin default & named arguments helps Sign up form with field input validation 49
  50. Too long parameter list @Composable fun SignUpContent( isProcessing: Boolean, emailAddress:

    String, isEmailError: Boolean, onEmailChange: (String) -> Unit = {}, validateEmail: () -> Unit = {}, phoneNumber: String, isPhoneInvalid: Boolean,onPhoneChange: (String) -> Unit = {},validatePhone: () -> Unit = {}, hasAcceptedToC: Boolean,onAcceptChange: (Boolean) -> Unit = {}, signUpUser: () -> Unit = {}, ) 50
  51. Reduce parameter list @Composable fun SignUpContent( isProcessing: Boolean, canSubmitForm: Boolean,

    signUpUser: () -> Unit = {}, emailInputField: @Composable () -> Unit = {}, phoneNumberField: @Composable () -> Unit = {}, checkToSAndPrivacyPolicyField: @Composable () -> Unit = {}, 51
  52. Possible problems Too much state to hoist (too many parameters

    -> code smell). Increased amount of logic to perform in composable functions. 52
  53. Solutions: State Holders Delegate the logic and state responsibilities to

    other classes: state holders. (aka hoisted state objects) State holders manage logic and state of composables. 53
  54. More than one way to hold state - Source of

    truth Composable Plain class state holder ViewModel 54
  55. State holders - find what fits best State holders are

    compoundable: one state holder can contain another Depending on the scope of the corresponding UI, can have various sizes Can be a plain object, a ViewModel or both There can be multiple state holders for one composable Plain state holders may depend on ViewModel if it is necessary to access business logic or screen state 55
  56. State holders - an example 56

  57. More than one way to hold state (source of truth)

    • Composables ◦ Simple UI state management • Plain class state holder: ◦ Complex UI state management ◦ State of UI elements (ex: ScaffoldState, DrawerState, etc…) ◦ Created and remembered in Composition ◦ Follow composable lifecycle 57
  58. More than one way to hold state (source of truth):

    ViewModel 58 • Special type of state holder • Provide access to business logic • Longer lifecycle than the composition (survive configuration changes) • Do not keep long-lived references to state that is bound to the lifecycle of the Composition ⇒ Memory Leaks. Use SavedStateHandle to preserve state across process recreation • Operations triggered by ViewModel survive configuration changes
  59. Cohesion and loose coupling Cohesion measures the degree of connectivity

    among the elements of a single module (and for object-oriented design, a single class or object) Avoid coincidental cohesion, aim for functional cohesion. Coupling is the dependency among units in different modules and reflects the ways in which parts of one module influence parts of other modules 59
  60. Law of Demeter - principle of least knowledge • Each

    unit should have only limited knowledge about other units: only units "closely" related to the current unit. • Each unit should only talk to its friends; don't talk to strangers. • Only talk to your immediate friends. • You can play by yourself • You can create and play with the toys you make • Share your toys only with close friends and no further. 60
  61. Resources & Further reading 1. State and Jetpack Compose |

    Android Developers 2. Lifecycle of composables | Jetpack Compose | Android Developers 3. Side-effects in Compose | Jetpack Compose | Android Developers 4. Theming in Compose - Jetpack 5. Navigating with Compose | Jetpack Compose | Android Developers Icons: • Pulley, icon 54 • Pulley System, Phạm Thanh Lộc 61
  62. Compose Yourself! Part 3 - Adaptive designs 62

  63. Expand the UI implementation using best practices for adaptable designs,

    different material navigation components and optimal screen space usage. Part 3: Large screen support 63
  64. Three tiers 64 https://developer.android.com/docs/quality-guidelines/large-screen-app-quality Tier 3 Basic Large screen ready

      Tier 2 Better Large screen optimized   Tier 1 Best Large screen differentiated  
  65. Tier 3 of large screen support 65 • Users can

    complete critical flows but with a less than optimal user experience. • Apps run full screen (or full window in multi- window mode), but app layout might not be ideal. • Basic support for external input devices (keyboard, mouse, and trackpad) is provided.
  66. Tier 2 of large screen support 66 • Apps implement

    layout optimizations for all screen sizes and device configurations. • External input device support is enhanced.
  67. Tier 1 of large screen support 67 • Apps provide

    a user experience designed for tablets, foldables, and Chrome OS. • Support is provided for multi-tasking, foldable postures, drag and drop, and stylus input.
  68. Large screen optimization made easy Expand the UI implementation using

    best practices for adaptable designs, different material navigation components and optimal screen space usage. 68 Doesn’t look great!
  69. Large screen optimization made easy 69

  70. Workshop objective 70 Phone Foldable unfolded Tablet / desktop Adaptive

    designs & material navigations Optimal screen space usage
  71. Window Size Classes 71 Both width and height are classified

    separately, so at any point in time, your app has two window size classes—one for width and one for height. Available width is usually more important than available height due to the ubiquity of vertical scrolling, so for this case you'll also use width size classes.
  72. WindowSizeClass dependencies { implementation "androidx.compose.material3:material3-window-size-class:[version]" } 72 dependencies { implementation

    Libs.AndroidX.Compose.windowSizeClass }
  73. WindowSizeClass class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) setContent { GreenThumbsTheme { val windowSize = calculateWindowSizeClass(this) NavGraph(windowSize.widthSizeClass) } } } } 73
  74. @Composable fun NavGraph(windowSize: WindowWidthSizeClass) { … } fun WelcomeScreen( navigate:

    (String) -> Unit = {}, windowSize: WindowWidthSizeClass, ) = … WindowSizeClass 74
  75. WindowSizeClass Introduce some WindowSize specific padding. For example: Modifier .padding(horizontal

    = when (windowSize) { WindowWidthSizeClass.Compact -> 16.dp else -> 72.dp }) 75
  76. WindowSizeClass Can you also update the @Previews to show a

    phone and tablet preview? private fun Welcome_Preview() = … private fun Welcome_Preview_Tablet() = ... 76
  77. WindowInsets 77 Where’d the button go?

  78. WindowInsets fun WelcomeScreen( navigate: (String) -> Unit = {}, windowSize:

    WindowWidthSizeClass, ) = Scaffold { padding -> Column( Modifier … .systemBarsPadding() ) { … } } 78 Note: Accommodates both top and bottom insets; navigationBarsPadding() only accommodates the navigation bar.
  79. Optimizing screen space 79 OverviewListCompact OverviewListGrid OverviewListGrid

  80. Navigation components 80 NavigationBar ModalNavigationDrawer PermanentNavigationDrawer

  81. Navigation components Three placeholders have been created: • PlantOverviewNavDrawerContent() for

    the navigation drawer • PlantOverviewNavRailContent() for the navigation rail • PlantOverviewNavBarContent() for the navigation bar 81
  82. Resources & Further reading Large screen app quality | Android

    Developers 82 Learn about foldables | Android Developers Build adaptive layouts | Android Developers