$30 off During Our Annual Pro Sale. View Details »

DroidKaigi: Android UI: Patterns, Practices, Pitfalls

Chris Horner
February 21, 2020

DroidKaigi: Android UI: Patterns, Practices, Pitfalls

Nearing the end of its lifespan, Android's UI framework is now chock-full of useful tools for building UIs. Sometimes it seems like there's a hundred ways to achieve something. Other times you have no idea where to even begin.

This talk outlines when and when not to apply certain UI components. It discusses tips and tricks Android developers can apply when building their UIs, as well as outlines some of the common gotchas to avoid. Topics discussed will include:

Animations and Transitions
Layout hierarchies
Not using ConstraintLayout for everything
Drawables
Custom Views/ViewGroups
Threading and timing
Themes and styles

Chris Horner

February 21, 2020
Tweet

More Decks by Chris Horner

Other Decks in Programming

Transcript

  1. Android UI
    Patterns, Practices, Pitfalls
    @chris_h_codes

    View Slide

  2. View Slide

  3. View Slide

  4. https:/
    /github.com/aosp-mirror/.../View.java

    View Slide

  5. CSS
    IS
    AWESOME

    View Slide

  6. ViewGroup View

    View Slide

  7. override fun onLayout(
    changed: Boolean,
    left: Int,
    top: Int,
    right: Int,
    bottom: Int
    )
    ViewGroup
    override fun onMeasure(
    widthSpec: Int,
    heightSpec: Int
    )\

    View Slide

  8. FrameLayout
    AbsoluteLayout
    LinearLayout
    RelativeLayout
    ListView

    View Slide

  9. GridLayout
    ConstraintLayout
    CoordinatorLayout
    NestedScrollView
    RecyclerView

    View Slide

  10. ConstraintLayout

    View Slide


  11. android:id="@+id/first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    />
    android:id="@+id/second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@idfirst"
    />

    View Slide

  12. android:orientation="vertical"
    >
    android:id="@+id/first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    />
    android:id="@+id/second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    />

    View Slide

  13. View Slide

  14. android:background="pink"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    />
    android:background="orange"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    />
    android:background="green"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    />

    View Slide

  15. android:background="pink"
    />
    android:background="orange"
    android:layout_gravity="center"
    />
    android:background="green"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    />


    View Slide

  16. Text label
    android:orientation=“horizontal"
    >
    This tag and its children can be replaced by one and a compound drawable

    View Slide

  17. override fun onMeasure(
    widthSpec: Int,
    heightSpec: Int
    )/
    1x
    1x 1x
    1x 1x 1x 1x 1x

    View Slide

  18. override fun onMeasure(
    widthSpec: Int,
    heightSpec: Int
    )/
    1x
    2x 1x
    2x 2x 1x 1x 1x

    View Slide

  19. override fun onMeasure(
    widthSpec: Int,
    heightSpec: Int
    )/
    2x
    4x 2x
    4x 4x 2x 2x 2x

    View Slide








  20. View Slide


  21. View Slide

  22. ConstraintLayout

    View Slide

  23. ConstraintLayout ConstraintLayout

    View Slide

  24. ConstraintLayout ConstraintLayout

    View Slide

  25. Main container
    Screen container
    Screen root
    Recycler
    List children

    View Slide

  26. Be mindful of your ConstraintLayouts

    View Slide

  27. Be mindful of your MotionLayouts

    View Slide

  28. Animation Complexity

    View Slide

  29. Animation Complexity
    view.animate()
    .translationX(100f)
    .alpha(0.5f)
    .rotation(45f)
    Easy Pretty tricky

    View Slide

  30. Animation Complexity
    view.animate()
    .translationX(100f)
    .alpha(0.5f)
    .rotation(45f)
    Easy Pretty tricky
    android:animateLayoutChanges="true"

    View Slide

  31. Animation Complexity
    view.animate()
    .translationX(100f)
    .alpha(0.5f)
    .rotation(45f)
    Easy Pretty tricky
    android:animateLayoutChanges="true"
    viewGroup
    .layoutTransition
    .enableTransitionType(LayoutTransition.CHANGING)

    View Slide

  32. Animation Complexity
    view.animate()
    .translationX(100f)
    .alpha(0.5f)
    .rotation(45f)
    Easy Pretty tricky
    ObjectAnimator.ofFloat(0f, 1f)\
    android:animateLayoutChanges="true"

    View Slide

  33. Animation Complexity
    view.animate()
    .translationX(100f)
    .alpha(0.5f)
    .rotation(45f)
    Easy Pretty tricky
    ObjectAnimator.ofFloat(object : Property ...)\
    android:animateLayoutChanges="true"

    View Slide

  34. Animation Complexity
    view.animate()
    .translationX(100f)
    .alpha(0.5f)
    .rotation(45f)
    Easy Pretty tricky
    android:animateLayoutChanges="true" ObjectAnimator.ofFloat(...)\


    View Slide

  35. Animation Complexity
    view.animate()
    .translationX(100f)
    .alpha(0.5f)
    .rotation(45f)
    Easy Pretty tricky
    android:animateLayoutChanges="true" ObjectAnimator.ofFloat(...)\


    TransitionManager
    .beginDelayedTransition(viewGroup)

    View Slide

  36. Animation Complexity
    view.animate()
    .translationX(100f)
    .alpha(0.5f)
    .rotation(45f)
    Easy Pretty tricky
    android:animateLayoutChanges="true" ObjectAnimator.ofFloat(...)\


    TransitionManager
    .beginDelayedTransition(vie

    View Slide

  37. MotionLayout
    Easy to use, declarative syntax
    Seekable
    Can be driven by touch

    View Slide

  38. MotionLayout
    Easy to use, declarative syntax
    Seekable
    Can be driven by touch
    All animated views have to be direct children
    Can’t easily perform shared element transitions

    View Slide

  39. Transition Tips

    View Slide

  40. Transition Tips
    1. Make them quick, debug them slow

    View Slide

  41. View Slide

  42. View Slide

  43. View Slide

  44. Transition Tips
    1. Make them quick, debug them slow
    2. Choreograph customisations with Kotlin

    View Slide

  45. class SomeFragment : Fragment() {
    sharedElementEnterTransition = TransitionInflater
    .from(context)
    .inflateTransition(android.R.transition.move)
    }

    View Slide

  46. sharedElementEnterTransition = TransitionInflater
    .from(context)
    .inflateTransition(android.R.transition.move)






    View Slide

  47. Transitions can…
    Target specific views
    Exclude specific views
    Have their duration, interpolation, and pathing modified

    View Slide

  48. xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="200"
    >
    android:interpolator="@android:interpolator/fast_out_slow_in"
    android:matchOrder="@string/transitionName_currentFeelingButton"
    >



    android:interpolator="@android:interpolator/fast_out_linear_in"
    android:matchOrder="@string/transitionName_feelingTitle"
    >





    View Slide

  49. View Slide

  50. inline fun transitionSet(
    block: TransitionSet.() -> Unit
    ): TransitionSet {
    return TransitionSet().apply(block)
    }
    inline fun TransitionSet.addSet(
    block: TransitionSet.() -> Unit
    ) {
    addTransition(TransitionSet().apply(block))
    }
    inline fun TransitionSet.addTransition(
    transition: Transition,
    block: Transition.() -> Unit
    ) {
    transition.apply(block)
    addTransition(transition)
    }

    View Slide

  51. val transition = transitionSet {
    duration = 250
    addSet {
    addTransition(ChangeBounds())
    addTransition(ChangeTransform())
    addTarget(currentFeelingTransitionName)
    interpolator = FastOutSlowInInterpolator()
    }
    addSet {
    addTransition(ChangeBounds()) { pathMotion = ArcMotion() }
    addTransition(ChangeTransform())
    addTransition(TextResize())
    addTarget(titleTransitionName)
    interpolator = FastOutLinearInInterpolator()
    }
    }

    View Slide

  52. Transition Tips
    1. Make them quick, debug them slow
    2. Choreograph customisations with Kotlin
    3. Respect your parents

    View Slide

  53. val transition = transitionSet {
    duration = 250
    addSet {
    addTransition(ChangeBounds())
    addTarget(viewA)
    interpolator = FastOutSlowInInterpolator()
    }/
    addSet {
    addTransition(ChangeBounds()) { pathMotion = ArcMotion() }
    addTransition(TextResize())
    addTarget(viewB)
    interpolator = FastOutLinearInInterpolator()
    }/
    }/

    View Slide

  54. val transition = transitionSet {
    duration = 250
    addSet {
    addTransition(ChangeBounds())
    addTransition(ChangeTransform())
    addTarget(viewA)
    interpolator = FastOutSlowInInterpolator()
    }/
    addSet {
    addTransition(ChangeBounds()) { pathMotion = ArcMotion() }
    addTransition(ChangeTransform())
    addTransition(TextResize())
    addTarget(viewB)
    interpolator = FastOutLinearInInterpolator()
    }/
    }/

    View Slide

  55. android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/viewBackground"
    >


    View Slide

  56. android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/viewBackground"
    android:transitionGroup="false"
    >


    View Slide

  57. android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/viewBackground"
    android:transitionGroup="false"
    android:clipChildren="false"
    >


    View Slide

  58. Transition Tips
    1. Make them quick, debug them slow
    2. Choreograph customisations with Kotlin
    3. Respect your parents
    4. Remember that arc motion is a thing

    View Slide

  59. View Slide

  60. addTransition(ChangeBounds()) {
    pathMotion = ArcMotion()
    }/

    View Slide

  61. addTransition(ChangeBounds()) {
    pathMotion = ArcMotionPlus()
    }/

    View Slide

  62. View Slide

  63. https:/
    /github.com/neild001/ArcMotionPlus
    Using the ArcMotionPlus
    Using the ArcMotionPlus is straightforward. There are a just two settings
    • Arc Angle
    • Reflect
    Neil Davies
    neild001

    View Slide

  64. https:/
    /gist.github.com/chris-horner/80837fd3f3ac54052b766648d80ddbd2
    class ArcMotionPlus(private val arcAngle: Float = 90f, private val reflected: Boolean = false) : PathMotion() {
    override fun getPath(startX: Float, startY: Float, endX: Float, endY: Float): Path {
    val start = PointF(startX, startY)
    val end = PointF(endX, endY)
    require(!(start.x == end.x && start.y == end.y)) { "Start and end points cannot be the same." }
    require(arcAngle in 1.0..179.0) { "Arc angle must be between 1 and 179 degrees." }
    val angleRadians: Double = Math.toRadians(arcAngle.toDouble())
    val deltaX: Float = start.x - end.x
    val deltaY: Float = start.y - end.y
    val halfChordLength: Float = sqrt((deltaX * deltaX + deltaY * deltaY).toDouble()).toFloat() / 2f
    val radius: Float = halfChordLength / sin(angleRadians / 2f).toFloat()
    // The length of the line from the start or end point to the control point.
    chris-horner / ArcMotionPlus.kt

    View Slide

  65. Transition Tips
    1. Make them quick, debug them slow
    2. Choreograph customisations with Kotlin
    3. Respect your parents
    4. Remember that arc motion is a thing

    View Slide

  66. ScrollView Shenanigans

    View Slide

  67. ScrollView Shenanigans
    android:fillViewport="true"
    >

    View Slide

  68. View Slide

  69. View Slide

  70. View Slide


  71. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >





    View Slide


  72. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >





    View Slide


  73. android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    />

    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    />

    View Slide

  74. android:fillViewport="true"
    >


    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    />

    View Slide

  75. android:fillViewport="true"
    >


    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    />

    View Slide

  76. Forget ActionBar

    View Slide

  77. android:theme="@style/Theme.MaterialComponents.DayNight"
    />

    View Slide

  78. android:theme="@style/Theme.MaterialComponents.DayNight"\
    />

    View Slide

  79. android:theme="@style/Theme.MaterialComponents.DayNight.DarkActionBar"\
    />

    View Slide

  80. android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"\
    />

    View Slide

  81. android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"\
    />
    AndroidManifest.xml
    android:orientation="vertical"
    >
    android:id="@+id/toolbar"
    />

    activity_main.xml MainActivity.kt
    setSupportActionBar(toolbar)

    View Slide

  82. Toolbar
    Fragment A

    View Slide

  83. Title A
    Fragment A Fragment B

    View Slide

  84. Title B
    Fragment A Fragment B

    View Slide

  85. Title B
    Fragment B
    override fun onViewCreated() {
    setHasOptionsMenu(true)
    }
    override fun onCreateOptionsMenu() {
    super.onCreateOptionsMenu(menu, inflater)
    menu.clear()
    inflater.inflate(R.menu.screen_b, menu)
    }

    View Slide

  86. Title B

    View Slide

  87. View Slide

  88. Title B

    View Slide

  89. View Slide

  90. Don’t

    View Slide

  91. app:menu="@menu/menu"
    app:title="@string/title"
    />

    View Slide

  92. transitionSet {
    addTransition(ChangeBounds()) {
    addTarget(toolbar)
    }
    addTransition(Slide()) {
    addTarget(fromView)
    addTarget(toView)
    }
    }

    View Slide

  93. Styling Toolbars

    View Slide

  94. android:orientation="vertical"
    >
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:title="My toolbar"
    />
    android:id="@+id/content"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    />

    View Slide

  95. android:orientation="vertical"
    >
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?attr/colorPrimary"
    app:title="My toolbar"
    />
    android:id="@+id/content"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    />

    View Slide

  96. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:title="My toolbar"
    />

    View Slide

  97. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:title="My toolbar"
    />

    View Slide

  98. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:title="My toolbar"
    />

    View Slide

  99. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:title="My toolbar"
    style="@style/Widget.MaterialComponents.Toolbar.PrimarySurface"
    />

    View Slide

  100. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:title="My toolbar"
    style="@style/Widget.MaterialComponents.Toolbar.PrimarySurface"
    />
    name="Theme.MyApp"
    parent="Theme.MaterialComponents.DayNight.NoActionBar"
    >
    @color/blue_700
    @color/blue_700_variant
    @color/blue_700_variant
    @android:color/white
    @android:color/white

    View Slide

  101. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:title="My toolbar"
    style="@style/Widget.MaterialComponents.Toolbar.PrimarySurface"
    />
    name="Theme.MyApp"
    parent="Theme.MaterialComponents.DayNight.NoActionBar"
    >
    @color/blue_700
    @color/blue_700_variant
    @color/blue_700_variant
    @android:color/white
    @android:color/white

    View Slide

  102. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:title="My toolbar"
    />

    View Slide

  103. app:titleTextColor="@android:color/white"
    android:background="@color/background"
    />
    Basically, never do this

    View Slide

  104. Respect Themes

    View Slide

  105. android:background="@color/brandPrimary"\

    View Slide

  106. android:background="?attr/colorPrimary"\

    View Slide

  107. android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0"
    android:tint="?attr/colorControlNormal"
    >

    View Slide

  108. android:theme="@style/Theme.MyApp.Dark"
    >
    android:textAppearance="?attr/textAppearanceBody1"
    />

    android:theme="@style/Theme.MyApp"
    >
    android:textAppearance="?attr/textAppearanceBody1"
    />

    View Slide

  109. Required Watching
    https://www.youtube.com/watch?v=Owkf8DhAOSo

    View Slide

  110. Inset Issues

    View Slide

  111. Inset Issues
    https://www.youtube.com/watch?v=_mGDMVRO3iE
    BECOMING A MASTER
    WINDOW FITTER
    @chrisbanes

    View Slide

  112. Is one of my parent ViewGroups “insets aware”?
    How far down do I need to put fitsSystemWindows="true"?
    How do I cope with cumulative padding?
    Questions I Always Had

    View Slide

  113. Give up

    View Slide

  114. Always render edge-to-edge
    Give up

    View Slide

  115. override fun onCreate(savedInstanceState: Bundle?) {
    // Render under the status and navigation bars.
    window.decorView.systemUiVisibility =
    window.decorView.systemUiVisibility or
    View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
    View.SYSTEM_UI_FLAG_LAYOUT_STABLE
    }
    Always render edge-to-edge

    View Slide

  116. View Slide

  117. toolbar.updatePaddingWithInsets(top = true)
    bottomNav.updatePaddingWithInsets(bottom = true)

    View Slide

  118. https:/
    /gist.github.com/chris-horner/4718402eb6ebd4d89b63491245a359ff
    fun View.updatePaddingWithInsets(left: Boolean = false,
    top: Boolean = false,
    right: Boolean = false,
    bottom: Boolean = false) {
    doOnApplyWindowInsets { insets, padding ->
    updatePadding(left = if (left) padding.left + insets.systemWindowInsetLeft else padding.left,
    top = if (top) padding.top + insets.systemWindowInsetTop else padding.top,
    right = if (right) padding.right + insets.systemWindowInsetRight else padding.right,
    bottom = if (bottom) padding.bottom + insets.systemWindowInsetBottom else padding.bottom)
    }
    }
    inline fun View.doOnApplyWindowInsets(crossinline block: (insets: WindowInsets, padding: Rect) -> Unit) {
    // Create a snapshot of padding.
    val initialPadding = Rect(paddingLeft, paddingTop, paddingRight, paddingBottom)
    // Set an actual OnApplyWindowInsetsListener which proxies to the given lambda, also passing in the original padding.
    chris-horner / InsetUtils.kt

    View Slide

  119. https:/
    /github.com/chrisbanes/insetter

    View Slide

  120. https:/
    /github.com/material-components/material-components-android/releases

    View Slide

  121. View Slide

  122. Beware the Shadow Barber

    View Slide

  123. View Slide

  124. View Slide

  125. View Slide

  126. View Slide

  127. View Slide

  128. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="vertical"
    >

    View Slide

  129. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:clipToPadding="false"
    android:orientation="vertical"
    >

    View Slide

  130. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:clipToPadding="false"
    android:clipChildren="false"
    android:orientation="vertical"
    >

    View Slide

  131. Drawable Digressions

    View Slide

  132. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="vertical"
    >

    View Slide

  133. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="vertical"
    >



    View Slide

  134. Dynamic?

    View Slide

  135. Dynamic?

    View Slide

  136. class CustomLayout : LinearLayout {
    fun display(items: List) {
    for (index in items.indices) {
    val view: View = inflate(R.layout.item)
    if (index != 0) {
    val params = view.layoutParams
    params.topMargin = dpToPx(16)
    }
    addView(view)
    }
    }
    }

    View Slide

  137. class CustomLayout : LinearLayout {
    fun display(items: List) {
    for (index in items.indices) {
    val view: View = inflate(R.layout.item)
    if (index != 0) {
    val space = Space(context)
    space.layoutParams.height = dpToPx(16)
    addView(space)
    }
    addView(view)
    }
    }
    }

    View Slide

  138. What if…?
    android:separator="..."
    >

    View Slide

  139. We do have…
    android:divider="@drawable"
    >

    View Slide


  140. android:width="16dp"
    android:height="16dp"
    />
    android:color=“@android:color/transparent"
    />

    View Slide

  141. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="vertical"
    android:divider="@drawable/divider"
    >

    View Slide

  142. android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="vertical"
    android:divider="@drawable/divider"
    >

    View Slide

  143. Recycler Wrongdoings

    View Slide

  144. View Slide

  145. View Slide

  146. View Slide

  147. Generally, as long as you give views an
    ID they’ll try and restore their state

    View Slide

  148. You’re in a race!

    View Slide

  149. fun onRestoreInstanceState(state: Parcelable)
    You’re in a race!

    View Slide

  150. dataStream
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { items ->
    adapter.setItems(items)
    }\

    View Slide

  151. dataStream
    .replay(1)
    .refCount()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { items ->
    adapter.setItems(items)
    }\

    View Slide

  152. dataStream
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .replay(1)
    .refCount()
    .subscribe { items ->
    adapter.setItems(items)
    }\

    View Slide

  153. .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .replay(1)
    .refCount()
    .subscribe { items ->
    adapter.setItems(items)
    }\
    val dataStream: Flow>

    View Slide

  154. dataStream
    .onEach { items ->
    display(items)
    }
    .launchIn(scope)

    View Slide

  155. dataStream
    .onEach { items ->
    display(items)
    }
    .launchIn(scope)
    val scope = ???

    View Slide

  156. dataStream
    .onEach { items ->
    display(items)
    }
    .launchIn(scope)
    val scope = viewModelScope

    View Slide

  157. dataStream
    .onEach { items ->
    display(items)
    }
    .launchIn(scope)
    val scope = MainScope()

    View Slide

  158. /**
    * Creates the main [CoroutineScope] for UI components.
    */
    val scope = MainScope()

    View Slide

  159. val scope = MainScope()
    ContextScope(SupervisorJob() + Dispatchers.Main)

    View Slide

  160. val scope = MainScope()
    val scope = viewModelScope
    ContextScope(SupervisorJob() + Dispatchers.Main)

    View Slide

  161. val scope = MainScope()
    val scope = viewModelScope
    ContextScope(SupervisorJob() + Dispatchers.Main)
    SupervisorJob() + Dispatchers.Main.immediate

    View Slide

  162. Attach and put data in your adapter right
    after your views are inflated

    View Slide

  163. What Takeaways?

    View Slide

  164. Takeaways
    ConstraintLayout and MotionLayout are great! But remember
    there are alternatives
    Widgets sometimes have surprising properties
    Handle Toolbars and window insets manually
    Understand and get the most out of the theming system
    Watch out for gotchas when it comes to state restoration and
    rendering

    View Slide

  165. chris_h_codes
    chris-horner
    chrishorner.codes
    Android UI
    Patterns, Practices, Pitfalls

    View Slide