Save 37% off PRO during our Black Friday Sale! »

DroidKaigi: Android UI: Patterns, Practices, Pitfalls

7b1e567c19126de48554fe8e5e767395?s=47 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

7b1e567c19126de48554fe8e5e767395?s=128

Chris Horner

February 21, 2020
Tweet

Transcript

  1. Android UI Patterns, Practices, Pitfalls @chris_h_codes

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

  5. CSS IS AWESOME

  6. ViewGroup View

  7. override fun onLayout( changed: Boolean, left: Int, top: Int, right:

    Int, bottom: Int ) ViewGroup override fun onMeasure( widthSpec: Int, heightSpec: Int )\
  8. FrameLayout AbsoluteLayout LinearLayout RelativeLayout ListView

  9. GridLayout ConstraintLayout CoordinatorLayout NestedScrollView RecyclerView

  10. ConstraintLayout

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

    android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@idfirst" /> </ConstraintLayout>
  12. <LinearLayout android:orientation="vertical" > <TextView android:id="@+id/first" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/second"

    android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
  13. None
  14. <View android:background="pink" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <View android:background="orange" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"

    app:layout_constraintBottom_toBottomOf="parent" /> <View android:background="green" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" />
  15. <View android:background="pink" /> <View android:background="orange" android:layout_gravity="center" /> <View android:background="green" android:layout_width="wrap_content"

    android:layout_height="wrap_content" android:layout_gravity="bottom|end" /> <FrameLayout> </FrameLayout>
  16. Text label <LinearLayout android:orientation=“horizontal" > This tag and its children

    can be replaced by one <TextView/> and a compound drawable
  17. override fun onMeasure( widthSpec: Int, heightSpec: Int )/ 1x 1x

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

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

    2x 4x 4x 2x 2x 2x
  20. <RelativeLayout> <LinearLayout> <RelativeLayout> <FrameLayout> <RelativeLayout> <RelativeLayout> <LinearLayout>

  21. <ConstraintLayout>

  22. ConstraintLayout

  23. ConstraintLayout ConstraintLayout

  24. ConstraintLayout ConstraintLayout

  25. Main container Screen container Screen root Recycler List children

  26. Be mindful of your ConstraintLayouts

  27. Be mindful of your MotionLayouts

  28. Animation Complexity

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

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

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

    viewGroup .layoutTransition .enableTransitionType(LayoutTransition.CHANGING)
  32. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky ObjectAnimator.ofFloat(0f,

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

    : Property<View, Float> ...)\ android:animateLayoutChanges="true"
  34. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky android:animateLayoutChanges="true"

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

    ObjectAnimator.ofFloat(...)\ <MotionScene> </MotionScene> TransitionManager .beginDelayedTransition(viewGroup)
  36. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky android:animateLayoutChanges="true"

    ObjectAnimator.ofFloat(...)\ <MotionScene> </MotionScene> TransitionManager .beginDelayedTransition(vie
  37. MotionLayout Easy to use, declarative syntax Seekable Can be driven

    by touch
  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
  39. Transition Tips

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

  41. None
  42. None
  43. None
  44. Transition Tips 1. Make them quick, debug them slow 2.

    Choreograph customisations with Kotlin
  45. class SomeFragment : Fragment() { sharedElementEnterTransition = TransitionInflater .from(context) .inflateTransition(android.R.transition.move)

    }
  46. sharedElementEnterTransition = TransitionInflater .from(context) .inflateTransition(android.R.transition.move) <transitionSet> <changeBounds/> <changeTransform/> <changeClipBounds/> <changeImageTransform/>

    </transitionSet>
  47. Transitions can… Target specific views Exclude specific views Have their

    duration, interpolation, and pathing modified
  48. <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:duration="200" > <transitionSet android:interpolator="@android:interpolator/fast_out_slow_in" android:matchOrder="@string/transitionName_currentFeelingButton" > <changeBounds />

    <changeTransform /> </transitionSet> <transitionSet android:interpolator="@android:interpolator/fast_out_linear_in" android:matchOrder="@string/transitionName_feelingTitle" > <arcMotion android:maximumAngle="150f"/> <changeBounds/> <transition class="codes.chrishorner.tumtracker.animation.TextResize"/> </transitionSet> </transitionSet>
  49. None
  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) }
  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() } }
  52. Transition Tips 1. Make them quick, debug them slow 2.

    Choreograph customisations with Kotlin 3. Respect your parents
  53. val transition = transitionSet { duration = 250 addSet {

    addTransition(ChangeBounds()) addTarget(viewA) interpolator = FastOutSlowInInterpolator() }/ addSet { addTransition(ChangeBounds()) { pathMotion = ArcMotion() } addTransition(TextResize()) addTarget(viewB) interpolator = FastOutLinearInInterpolator() }/ }/
  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() }/ }/
  55. <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/viewBackground" > <!-- Child views to transition

    --> </FrameLayout>
  56. <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/viewBackground" android:transitionGroup="false" > <!-- Child views to

    transition --> </FrameLayout>
  57. <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/viewBackground" android:transitionGroup="false" android:clipChildren="false" > <!-- Child views

    to transition --> </FrameLayout>
  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
  59. None
  60. addTransition(ChangeBounds()) { pathMotion = ArcMotion() }/

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

  62. None
  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
  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
  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
  66. ScrollView Shenanigans

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

  68. None
  69. None
  70. None
  71. <ScrollView> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <View android:background="@color/pink" /> <View android:background="@color/orange"

    /> <View android:background="@color/green" /> </LinearLayout> </ScrollView>
  72. <ScrollView> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <View android:background="@color/pink" /> <View android:background="@color/orange"

    /> <View android:background="@color/green" /> </LinearLayout> </ScrollView>
  73. <View android:background="@color/pink" /> <Space android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <View android:background="@color/orange"

    /> <Space android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <View android:background="@color/green" />
  74. <ScrollView android:fillViewport="true" > <LinearLayout> <View android:background="@color/pink" /> <Space android:layout_width="match_parent" android:layout_height="0dp"

    android:layout_weight="1" /> <View android:background="@color/orange" />
  75. <ScrollView android:fillViewport="true" > <LinearLayout> <View android:background="@color/pink" /> <Space android:layout_width="match_parent" android:layout_height="0dp"

    android:layout_weight="1" /> <View android:background="@color/orange" />
  76. Forget ActionBar

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

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

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

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

  81. <application android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"\ /> AndroidManifest.xml <LinearLayout android:orientation="vertical" > <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" />

    </LinearLayout> activity_main.xml MainActivity.kt setSupportActionBar(toolbar)
  82. Toolbar Fragment A

  83. Title A Fragment A Fragment B

  84. Title B Fragment A Fragment B

  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) }
  86. Title B

  87. None
  88. Title B

  89. None
  90. Don’t

  91. <androidx.appcompat.widget.Toolbar app:menu="@menu/menu" app:title="@string/title" />

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

    } }
  93. Styling Toolbars

  94. <LinearLayout android:orientation="vertical" > <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" /> <ScrollView

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

    <ScrollView android:id="@+id/content" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> </LinearLayout>
  96. <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" />

  97. <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" />

    </com.google.android.material.appbar.AppBarLayout>
  98. <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" />

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

  100. <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" style="@style/Widget.MaterialComponents.Toolbar.PrimarySurface" /> <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar"

    > <item name="colorPrimary">@color/blue_700</item> <item name="colorPrimaryVariant">@color/blue_700_variant</item> <item name="colorPrimaryDark">@color/blue_700_variant</item> <item name="colorOnPrimary">@android:color/white</item> <item name="colorOnPrimarySurface">@android:color/white</item> </style>
  101. <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" style="@style/Widget.MaterialComponents.Toolbar.PrimarySurface" /> <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar"

    > <item name="colorPrimary">@color/blue_700</item> <item name="colorPrimaryVariant">@color/blue_700_variant</item> <item name="colorPrimaryDark">@color/blue_700_variant</item> <item name="colorOnPrimary">@android:color/white</item> <item name="colorOnPrimarySurface">@android:color/white</item> </style>
  102. <com.google.android.material.appbar.MaterialToolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" />

  103. <androidx.appcompat.widget.Toolbar app:titleTextColor="@android:color/white" android:background="@color/background" /> Basically, never do this

  104. Respect Themes

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

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

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

  108. <com.google.android.material.card.MaterialCardView android:theme="@style/Theme.MyApp.Dark" > <TextView android:textAppearance="?attr/textAppearanceBody1" /> </com.google.android.material.card.MaterialCardView> <com.google.android.material.card.MaterialCardView android:theme="@style/Theme.MyApp" >

    <TextView android:textAppearance="?attr/textAppearanceBody1" /> </com.google.android.material.card.MaterialCardView>
  109. Required Watching https://www.youtube.com/watch?v=Owkf8DhAOSo

  110. Inset Issues

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

  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
  113. Give up

  114. Always render edge-to-edge Give up

  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
  116. None
  117. toolbar.updatePaddingWithInsets(top = true) bottomNav.updatePaddingWithInsets(bottom = true)

  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
  119. https:/ /github.com/chrisbanes/insetter

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

  121. None
  122. Beware the Shadow Barber

  123. None
  124. None
  125. None
  126. None
  127. None
  128. <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:orientation="vertical" >

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

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

  131. Drawable Digressions

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

  133. <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:orientation="vertical" > <View android:layout_marginTop="0dp" /> <View

    android:layout_marginTop="16dp" /> <View android:layout_marginTop="16dp" />
  134. Dynamic?

  135. Dynamic?

  136. class CustomLayout : LinearLayout { fun display(items: List<Item>) { 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) } } }
  137. class CustomLayout : LinearLayout { fun display(items: List<Item>) { 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) } } }
  138. What if…? <LinearLayout android:separator="..." >

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

  140. <shape android:shape="rectangle"> <size android:width="16dp" android:height="16dp" /> <solid android:color=“@android:color/transparent" /> </shape>

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

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

  143. Recycler Wrongdoings

  144. None
  145. None
  146. None
  147. Generally, as long as you give views an ID they’ll

    try and restore their state
  148. You’re in a race!

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

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

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

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

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

    val dataStream: Flow<List<Item>>
  154. dataStream .onEach { items -> display(items) } .launchIn(scope)

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

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

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

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

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

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

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

    Dispatchers.Main) SupervisorJob() + Dispatchers.Main.immediate
  162. Attach and put data in your adapter right after your

    views are inflated
  163. What Takeaways?

  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
  165. chris_h_codes chris-horner chrishorner.codes Android UI Patterns, Practices, Pitfalls