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

Android Puzzlers: Traps, Pitfalls and Corner Cases

Android Puzzlers: Traps, Pitfalls and Corner Cases

Android is a marvellous platform that has been evolving for more than 10 years. Having such a large framework, so many methods and classes while maintaining legacy generally ends up with a price to pay: some parts of the SDK comes with their questions and puzzlers.

In this talk, we will present some of the most head-scratching API namings/usages/results and give an explanation for each of them.

Cyril Mottier

April 24, 2019
Tweet

More Decks by Cyril Mottier

Other Decks in Programming

Transcript

  1. Android
    Puzzlers
    Traps, Pitfalls and Corner Cases
    @cyrilmottier
    ?
    ?
    ?
    ?
    ?
    ?
    ?
    ?
    ?

    View Slide

  2. It all began with…

    View Slide

  3. View Slide

  4. Small applications with
    curious behavior
    Android puzzlers

    View Slide

  5. Small applications with
    curious behavior
    Android puzzlers
    1. What does it print?

    View Slide

  6. Small applications with
    curious behavior
    Android puzzlers
    1. What does it print?
    2. The solution revealed

    View Slide

  7. Small applications with
    curious behavior
    Android puzzlers
    1. What does it print?
    2. The solution revealed
    3. The moral

    View Slide

  8. A simple
    setup
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-android-extensions'
    android {
    compileSdkVersion 28
    defaultConfig {
    minSdkVersion 28
    targetSdkVersion 28
    versionCode 1
    versionName "1.0"
    }
    buildTypes {
    release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
    }
    }
    }
    dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.21"
    }
    common.gradle

    View Slide


  9. xmlns:tools="http://schemas.android.com/tools"
    package="com.cyrilmottier.android.puzzlers"
    tools:ignore="AllowBackup,GoogleAppIndexingWarning">
    android:allowBackup=“false"
    android:label=“@string/app_name“
    android:supportsRtl="true"
    android:theme="@style/android:Theme.Material.Light.DarkActionBar">








    A simple
    setup
    AndroidManifest.xml

    View Slide

  10. A simple
    setup
    minSdkVersion 28
    targetSdkVersion 28
    compileSdkVersion 28
    Running on Android 9 (Pie) / API 28
    Warnings/errors/lint turned off
    Focusing on vanilla-Android

    View Slide

  11. Inflation result

    View Slide


  12. android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="child" />
    MainActivity.kt
    layout/view_inflate.xml
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val parent = FrameLayout(this).apply {
    tag = "parent"
    }
    val result1 = layoutInflater.inflate(R.layout.view_inflate, parent)
    val result2 = layoutInflater.inflate(R.layout.view_inflate, null)
    trace("${result1.tag} and ${result2.tag}")
    }
    }

    View Slide


  13. android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="child" />
    MainActivity.kt
    layout/view_inflate.xml
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val parent = FrameLayout(this).apply {
    tag = "parent"
    }
    val result1 = layoutInflater.inflate(R.layout.view_inflate, parent)
    val result2 = layoutInflater.inflate(R.layout.view_inflate, null)
    trace("${result1.tag} and ${result2.tag}")
    }
    }

    View Slide

  14. A child and child
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val parent = FrameLayout(this).apply {
    tag = "parent"
    }
    val result1 = layoutInflater.inflate(R.layout.view_inflate, parent)
    val result2 = layoutInflater.inflate(R.layout.view_inflate, null)
    trace("${result1.tag} and ${result2.tag}")
    }
    }

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="child" />
    MainActivity.kt
    layout/view_inflate.xml

    View Slide

  15. B parent and child
    A child and child
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val parent = FrameLayout(this).apply {
    tag = "parent"
    }
    val result1 = layoutInflater.inflate(R.layout.view_inflate, parent)
    val result2 = layoutInflater.inflate(R.layout.view_inflate, null)
    trace("${result1.tag} and ${result2.tag}")
    }
    }

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="child" />
    MainActivity.kt
    layout/view_inflate.xml

    View Slide

  16. B parent and child
    C throws NullPointerException
    A child and child
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val parent = FrameLayout(this).apply {
    tag = "parent"
    }
    val result1 = layoutInflater.inflate(R.layout.view_inflate, parent)
    val result2 = layoutInflater.inflate(R.layout.view_inflate, null)
    trace("${result1.tag} and ${result2.tag}")
    }
    }

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="child" />
    MainActivity.kt
    layout/view_inflate.xml

    View Slide

  17. B parent and child
    C throws NullPointerException
    D Answer D
    A child and child
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val parent = FrameLayout(this).apply {
    tag = "parent"
    }
    val result1 = layoutInflater.inflate(R.layout.view_inflate, parent)
    val result2 = layoutInflater.inflate(R.layout.view_inflate, null)
    trace("${result1.tag} and ${result2.tag}")
    }
    }

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="child" />
    MainActivity.kt
    layout/view_inflate.xml

    View Slide

  18. B parent and child
    C throws NullPointerException
    D Answer D
    A child and child
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val parent = FrameLayout(this).apply {
    tag = "parent"
    }
    val result1 = layoutInflater.inflate(R.layout.view_inflate, parent)
    val result2 = layoutInflater.inflate(R.layout.view_inflate, null)
    trace("${result1.tag} and ${result2.tag}")
    }
    }

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="child" />
    MainActivity.kt
    layout/view_inflate.xml

    View Slide

  19. attaches child to parent
    & returns parent
    null
    null
    parent
    parent
    returns child
    root
    returns child
    attaches child to parent
    & returns parent
    Output

    View Slide

  20. Favor the 3-args inflate
    method instead
    fun LayoutInflater.inflate(
    resource: Int,
    root: ViewGroup,
    attachToRoot: Boolean
    ): View
    Be explicit

    View Slide

  21. null
    null
    parent
    parent
    false
    true
    false
    true
    returns child
    root attachToRoot
    returns child
    returns child
    attaches child to parent
    & returns parent
    Output

    View Slide

  22. null
    null
    parent
    parent
    false
    true
    false
    true
    returns child
    root attachToRoot
    returns child
    returns child
    attaches child to parent
    & returns parent
    Output

    View Slide

  23. Padding precedence

    View Slide

  24. MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    trace(
    "start=${text.paddingStart} top=${text.paddingTop} " +
    "end=${text.paddingEnd} bottom=${text.paddingBottom}"
    )
    }
    }

    View Slide

  25. MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    trace(
    "start=${text.paddingStart} top=${text.paddingTop} " +
    "end=${text.paddingEnd} bottom=${text.paddingBottom}"
    )
    }
    }

    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/padded_rect"
    android:padding="5px"
    android:paddingBottom="6px"
    android:paddingEnd="7px"
    android:paddingHorizontal="8px"
    android:paddingLeft="9px"
    android:paddingRight="10px"
    android:paddingStart="11px"/>
    layout/activity_main.xml

    View Slide

  26. MainActivity.kt

    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/padded_rect"
    android:padding="5px"
    android:paddingBottom="6px"
    android:paddingEnd="7px"
    android:paddingHorizontal="8px"
    android:paddingLeft="9px"
    android:paddingRight="10px"
    android:paddingStart="11px"/>


    android:right="3px" android:top="4px" />

    layout/activity_main.xml
    drawable/padded_rect.xml
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    trace(
    "start=${text.paddingStart} top=${text.paddingTop} " +
    "end=${text.paddingEnd} bottom=${text.paddingBottom}"
    )
    }
    }

    View Slide

  27. class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    trace(
    "start=${text.paddingStart} top=${text.paddingTop} " +
    "end=${text.paddingEnd} bottom=${text.paddingBottom}"
    )
    }
    }
    B start=9 top=5 end=10
    bottom=6
    C start=2 top=4 end=3
    bottom=1
    D start=11 top=5 end=7
    bottom=5
    A start=5 top=5 end=5
    bottom=5
    MainActivity.kt

    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/padded_rect"
    android:padding="5px"
    android:paddingBottom="6px"
    android:paddingEnd="7px"
    android:paddingHorizontal="8px"
    android:paddingLeft="9px"
    android:paddingRight="10px"
    android:paddingStart="11px"/>
    layout/activity_main.xml
    drawable/padded_rect.xml


    android:right="3px" android:top="4px" />

    View Slide



  28. android:right="3px" android:top="4px" />

    B start=9 top=5 end=10
    bottom=6
    C start=2 top=4 end=3
    bottom=1
    D start=11 top=5 end=7
    bottom=5
    A start=5 top=5 end=5
    bottom=5
    MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    trace(
    "start=${text.paddingStart} top=${text.paddingTop} " +
    "end=${text.paddingEnd} bottom=${text.paddingBottom}"
    )
    }
    }

    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/padded_rect"
    android:padding="5px"
    android:paddingBottom="6px"
    android:paddingEnd="7px"
    android:paddingHorizontal="8px"
    android:paddingLeft="9px"
    android:paddingRight="10px"
    android:paddingStart="11px"/>
    layout/activity_main.xml
    drawable/padded_rect.xml

    View Slide

  29. background Drawable padding
    supportsRtl = false
    Lower
    Lower
    Higher

    View Slide

  30. background Drawable padding
    padding[Start|End]
    Lower
    Lower
    Higher
    supportsRtl = false

    View Slide

  31. background Drawable padding
    padding[Start|End]
    Lower
    Lower
    Higher
    padding[Left|Top|Right|Bottom]
    supportsRtl = false

    View Slide

  32. padding[Left|Top|Right|Bottom]
    padding[Horizontal|Vertical]
    background Drawable padding
    padding[Start|End]
    Lower
    Lower
    Higher
    supportsRtl = false

    View Slide

  33. padding[Left|Top|Right|Bottom]
    padding[Horizontal|Vertical]
    padding
    background Drawable padding
    padding[Start|End]
    Lower
    Lower
    Higher
    supportsRtl = false

    View Slide

  34. padding[Left|Top|Right|Bottom]
    padding[Horizontal|Vertical]
    padding
    background Drawable padding
    padding[Start|End] padding[Left|Top|Right|Bottom]
    padding[Horizontal|Vertical]
    padding
    padding[Start|End]
    background Drawable padding
    supportsRtl = true
    Lower
    Higher
    supportsRtl = false

    View Slide

  35. Activity lifecycle

    View Slide

  36. MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    trace("onCreate")
    finish()
    trace("finish")
    }
    override fun onStart() {
    super.onStart()
    trace("onStart")
    }
    override fun onResume() {
    super.onResume()
    trace("onResume")
    }
    override fun onPause() {
    trace("onPause")
    super.onPause()
    }
    override fun onStop() {
    trace("onStop")
    super.onStop()
    }
    override fun onDestroy() {
    trace("onDestroy")
    super.onDestroy()
    }
    }

    View Slide

  37. B onCreate, finish, onStart,
    onResume, onPause, onStop,
    onDestroy
    C onCreate, onStart,
    onResume, onPause, onStop,
    onDestroy
    D onCreate, finish, onDestroy
    A onCreate
    MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    trace("onCreate")
    finish()
    trace("finish")
    }
    override fun onStart() {
    super.onStart()
    trace("onStart")
    }
    override fun onResume() {
    super.onResume()
    trace("onResume")
    }
    override fun onPause() {
    trace("onPause")
    super.onPause()
    }
    override fun onStop() {
    trace("onStop")
    super.onStop()
    }
    override fun onDestroy() {
    trace("onDestroy")
    super.onDestroy()
    }
    }

    View Slide

  38. B onCreate, finish, onStart,
    onResume, onPause, onStop,
    onDestroy
    C onCreate, onStart,
    onResume, onPause, onStop,
    onDestroy
    D onCreate, finish, onDestroy
    A onCreate
    MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    trace("onCreate")
    finish()
    trace("finish")
    }
    override fun onStart() {
    super.onStart()
    trace("onStart")
    }
    override fun onResume() {
    super.onResume()
    trace("onResume")
    }
    override fun onPause() {
    trace("onPause")
    super.onPause()
    }
    override fun onStop() {
    trace("onStop")
    super.onStop()
    }
    override fun onDestroy() {
    trace("onDestroy")
    super.onDestroy()
    }
    }

    View Slide

  39. View Slide

  40. “The following
    diagram shows
    the important
    state paths of
    an Activity.
    – d.android.com

    View Slide

  41. “The following
    diagram shows
    the important
    state paths of
    an Activity.
    – d.android.com

    View Slide

  42. Drawable tags

    View Slide

  43. MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val classNames = arrayOf(
    R.drawable.level_list,
    R.drawable.layer_list,
    R.drawable.shape
    ).map {
    getDrawable(it)::class.java.simpleName
    }
    trace(classNames.joinToString(", "))
    }
    }

    View Slide

  44. MainActivity.kt
    drawable/level_list.xml


    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val classNames = arrayOf(
    R.drawable.level_list,
    R.drawable.layer_list,
    R.drawable.shape
    ).map {
    getDrawable(it)::class.java.simpleName
    }
    trace(classNames.joinToString(", "))
    }
    }

    View Slide

  45. MainActivity.kt
    drawable/level_list.xml


    drawable/layer_list.xml


    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val classNames = arrayOf(
    R.drawable.level_list,
    R.drawable.layer_list,
    R.drawable.shape
    ).map {
    getDrawable(it)::class.java.simpleName
    }
    trace(classNames.joinToString(", "))
    }
    }

    View Slide

  46. MainActivity.kt
    drawable/level_list.xml


    drawable/layer_list.xml


    drawable/shape.xml


    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val classNames = arrayOf(
    R.drawable.level_list,
    R.drawable.layer_list,
    R.drawable.shape
    ).map {
    getDrawable(it)::class.java.simpleName
    }
    trace(classNames.joinToString(", "))
    }
    }

    View Slide

  47. B LevelDrawable,
    LayerListDrawable,
    ShapeDrawable
    C LevelListDrawable,
    LayerDrawable,
    GradientDrawable
    D throws InflateException
    A LevelListDrawable,
    LayerListDrawable,
    ShapeDrawable
    MainActivity.kt
    drawable/level_list.xml


    drawable/layer_list.xml


    drawable/shape.xml


    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val classNames = arrayOf(
    R.drawable.level_list,
    R.drawable.layer_list,
    R.drawable.shape
    ).map {
    getDrawable(it)::class.java.simpleName
    }
    trace(classNames.joinToString(", "))
    }
    }

    View Slide

  48. B LevelDrawable,
    LayerListDrawable,
    ShapeDrawable
    C LevelListDrawable,
    LayerDrawable,
    GradientDrawable
    D throws InflateException
    A LevelListDrawable,
    LayerListDrawable,
    ShapeDrawable
    MainActivity.kt
    drawable/level_list.xml


    drawable/layer_list.xml


    drawable/shape.xml


    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val classNames = arrayOf(
    R.drawable.level_list,
    R.drawable.layer_list,
    R.drawable.shape
    ).map {
    getDrawable(it)::class.java.simpleName
    }
    trace(classNames.joinToString(", "))
    }
    }

    View Slide

  49. The general rule
    ValarMorghulisDrawable

    View Slide

  50. The general rule

    View Slide

  51. The general rule













    LevelListDrawable
    TransitionDrawable
    RippleDrawable
    AdaptiveIconDrawable
    ColorDrawable
    VectorDrawable
    AnimatedVectorDrawable
    ScaleDrawable
    ClipDrawable
    RotateDrawable
    InsetDrawable
    BitmapDrawable
    NinePatchDrawable
    AnimatedImageDrawable
    xceptions

    View Slide

  52. Exceptions
    The general rule

    View Slide

  53. Exceptions





    StateListDrawable
    AnimatedStateListDrawable
    LayerDrawable
    AnimationDrawable
    GradientDrawable

    View Slide

  54. Exceptions





    StateListDrawable
    AnimatedStateListDrawable
    LayerDrawable
    AnimationDrawable
    GradientDrawable
    ShapeDrawable
    PaintDrawable
    PictureDrawable

    View Slide

  55. Exceptions






    StateListDrawable
    AnimatedStateListDrawable
    LayerDrawable
    AnimationDrawable
    GradientDrawable
    ShapeDrawable
    PaintDrawable
    PictureDrawable

    View Slide

  56. Exceptions







    StateListDrawable
    AnimatedStateListDrawable
    LayerDrawable
    AnimationDrawable
    GradientDrawable
    ShapeDrawable
    PaintDrawable
    PictureDrawable
    YourDrawable

    View Slide

  57. Fragment lifecycle

    View Slide

  58. TracingFragment.kt
    class TracingFragment : Fragment() {
    override fun onAttach(context: Context?) {
    super.onAttach(context)
    trace("onAttach")
    }
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    trace("onCreate")
    }
    override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
    ): View? {
    trace("onCreateView")
    return super.onCreateView(inflater, container, savedInstanceState)
    }
    override fun onDestroyView() {
    trace("onDestroyView")
    super.onDestroyView()
    }
    override fun onDestroy() {
    trace("onDestroy")
    super.onDestroy()
    }
    override fun onDetach() {
    trace("onDetach")
    super.onDetach()
    }
    }

    View Slide

  59. MainActivity.kt
    class MainActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val fragment = TracingFragment()
    supportFragmentManager.beginTransaction()
    .add(android.R.id.content, fragment, "fragment:puzzler")
    .commit()
    supportFragmentManager.beginTransaction()
    .detach(fragment)
    .commit()
    }
    override fun onSaveInstanceState(outState: Bundle?) {
    // For the simplicity of this puzzler,
    // let’s consider state is never ever saved
    }
    }

    View Slide

  60. B onAttach, onCreate,
    onCreateView,
    onDestroyView
    C onCreate, onDestroy
    D Nothing
    A onAttach, onCreate,
    onCreateView, onDestroyView,
    onDestroy, onDetach
    MainActivity.kt
    class MainActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val fragment = TracingFragment()
    supportFragmentManager.beginTransaction()
    .add(android.R.id.content, fragment, "fragment:puzzler")
    .commit()
    supportFragmentManager.beginTransaction()
    .detach(fragment)
    .commit()
    }
    override fun onSaveInstanceState(outState: Bundle?) {
    // For the simplicity of this puzzler,
    // let’s consider state is never ever saved
    }
    }

    View Slide

  61. B onAttach, onCreate,
    onCreateView,
    onDestroyView
    C onCreate, onDestroy
    D Nothing
    A onAttach, onCreate,
    onCreateView, onDestroyView,
    onDestroy, onDetach
    MainActivity.kt
    class MainActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val fragment = TracingFragment()
    supportFragmentManager.beginTransaction()
    .add(android.R.id.content, fragment, "fragment:puzzler")
    .commit()
    supportFragmentManager.beginTransaction()
    .detach(fragment)
    .commit()
    }
    override fun onSaveInstanceState(outState: Bundle?) {
    // For the simplicity of this puzzler,
    // let’s consider state is never ever saved
    }
    }

    View Slide

  62. add remove/replace attach/detach the Fragment
    and more…
    creates/destroys the Fragment’s
    associated View
    turns Fragment’s View visibility
    to GONE/VISIBLE
    Fragment actions
    attach detach
    show hide
    FragmentTransaction commands

    View Slide

  63. Notification channels

    View Slide

  64. MainActivity.kt
    private const val CHANNEL_ID = "channel"
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val manager = getSystemService(NotificationManager::class.java)
    val aChannel = NotificationChannel(
    CHANNEL_ID, // id
    "A name", // name
    NotificationManager.IMPORTANCE_DEFAULT // importance (3)
    )
    aChannel.lightColor = Color.RED
    manager.createNotificationChannel(aChannel)
    manager.deleteNotificationChannel(CHANNEL_ID)
    val anotherChannel = NotificationChannel(
    CHANNEL_ID, // id
    "Another name", // name
    NotificationManager.IMPORTANCE_HIGH // importance (4)
    )
    anotherChannel.lightColor = Color.GREEN
    manager.createNotificationChannel(anotherChannel)
    with(manager.getNotificationChannel(CHANNEL_ID)) {
    trace("$name, $importance, 0x${Integer.toHexString(lightColor)}")
    }
    }
    }

    View Slide

  65. MainActivity.kt
    private const val CHANNEL_ID = "channel"
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val manager = getSystemService(NotificationManager::class.java)
    val aChannel = NotificationChannel(
    CHANNEL_ID, // id
    "A name", // name
    NotificationManager.IMPORTANCE_DEFAULT // importance (3)
    )
    aChannel.lightColor = Color.RED
    manager.createNotificationChannel(aChannel)
    manager.deleteNotificationChannel(CHANNEL_ID)
    val anotherChannel = NotificationChannel(
    CHANNEL_ID, // id
    "Another name", // name
    NotificationManager.IMPORTANCE_HIGH // importance (4)
    )
    anotherChannel.lightColor = Color.GREEN
    manager.createNotificationChannel(anotherChannel)
    with(manager.getNotificationChannel(CHANNEL_ID)) {
    trace("$name, $importance, 0x${Integer.toHexString(lightColor)}")
    }
    }
    }
    B Another name, 4, 0xff00ff00
    C Another name, 3, 0xff00ff00
    D Another name, 3, 0xffff0000
    A A name, 3, 0xffff0000

    View Slide

  66. MainActivity.kt
    B Another name, 4, 0xff00ff00
    C Another name, 3, 0xff00ff00
    D Another name, 3, 0xffff0000
    A A name, 3, 0xffff0000
    private const val CHANNEL_ID = "channel"
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val manager = getSystemService(NotificationManager::class.java)
    val aChannel = NotificationChannel(
    CHANNEL_ID, // id
    "A name", // name
    NotificationManager.IMPORTANCE_DEFAULT // importance (3)
    )
    aChannel.lightColor = Color.RED
    manager.createNotificationChannel(aChannel)
    manager.deleteNotificationChannel(CHANNEL_ID)
    val anotherChannel = NotificationChannel(
    CHANNEL_ID, // id
    "Another name", // name
    NotificationManager.IMPORTANCE_HIGH // importance (4)
    )
    anotherChannel.lightColor = Color.GREEN
    manager.createNotificationChannel(anotherChannel)
    with(manager.getNotificationChannel(CHANNEL_ID)) {
    trace("$name, $importance, 0x${Integer.toHexString(lightColor)}")
    }
    }
    }

    View Slide

  67. fun NotificationManager
    .deleteNotificationChannel(
    channelId: String
    )

    View Slide

  68. It rather “hides”: the only
    possible deletion is app uninstall
    Doesn’t really delete
    fun NotificationManager
    .deleteNotificationChannel(
    channelId: String
    )

    View Slide

  69. Name/description Always modifiable
    Mainly for i18n purposes & user-defined names/descriptions.

    View Slide

  70. Name/description
    Importance
    Always modifiable
    Mainly for i18n purposes & user-defined names/descriptions.
    Modifiable under certain conditions
    Only if new importance is lower than the current one & none of the channel’s
    settings have been altered by the user.

    View Slide

  71. Name/description
    Importance
    Group
    Always modifiable
    Mainly for i18n purposes & user-defined names/descriptions.
    Modifiable under certain conditions
    Modifiable under certain conditions
    Only if the channel has no group yet.
    Only if new importance is lower than the current one & none of the channel’s
    settings have been altered by the user.

    View Slide

  72. Name/description
    Importance
    Group
    Other fields
    Always modifiable
    Mainly for i18n purposes & user-defined names/descriptions.
    Modifiable under certain conditions
    Only if new importance is lower than the current one & none of the channel’s
    settings have been altered by the user.
    Modifiable under certain conditions
    Only if the channel has no group yet.
    Never
    Changing seamlessly user-alterable settings would result in a bad UX.

    View Slide

  73. Preferences read/write

    View Slide

  74. MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val prefs = getSharedPreferences("test", Context.MODE_PRIVATE)
    prefs.edit()
    .putString("key", "hell")
    .apply()
    prefs.edit()
    .putString("key", null)
    .apply()
    val value = prefs.getString("key", "heaven")
    trace("Go to $value!")
    }
    }

    View Slide

  75. B Go to hell!
    C Go to null!
    D Either A, B or C
    A Go to heaven!
    MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val prefs = getSharedPreferences("test", Context.MODE_PRIVATE)
    prefs.edit()
    .putString("key", "hell")
    .apply()
    prefs.edit()
    .putString("key", null)
    .apply()
    val value = prefs.getString("key", "heaven")
    trace("Go to $value!")
    }
    }

    View Slide

  76. B Go to hell!
    C Go to null!
    D Either A, B or C
    A Go to heaven!
    MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val prefs = getSharedPreferences("test", Context.MODE_PRIVATE)
    prefs.edit()
    .putString("key", "hell")
    .apply()
    prefs.edit()
    .putString("key", null)
    .apply()
    val value = prefs.getString("key", "heaven")
    trace("Go to $value!")
    }
    }

    View Slide

  77. apply() is atomic
    async to disk

    View Slide

  78. async to disk
    apply() is
    atomic
    sync to memory

    View Slide

  79. sync to memory
    async to disk
    apply() is

    View Slide

  80. View Slide

  81. “ defValue -
    String: Value to
    return if this
    preference does
    not exist.

    View Slide

  82. ¯\_(ツ)_/¯
    Well…
    not exactly

    View Slide

  83. Null values are
    always considered
    as “not existing” so
    expect “defValue”

    View Slide

  84. LinearLayout weight

    View Slide

  85. MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    linear_layout.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
    trace(
    "LL: ${linear_layout.width}px, " +
    "TV1: ${text_view_1.width}px, " +
    "TV2: ${text_view_2.width}px"
    )
    }
    }
    }

    View Slide

  86. layout/activity_main.xml

    android:id="@+id/linear_layout"
    android:layout_width="400px"
    android:layout_height="match_parent">
    android:id="@+id/text_view_1"
    android:layout_width="300px"
    android:layout_height="match_parent"
    android:layout_weight="1" />
    android:id="@+id/text_view_2"
    android:layout_width="200px"
    android:layout_height="match_parent"
    android:layout_weight="2" />

    MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    linear_layout.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
    trace(
    "LL: ${linear_layout.width}px, " +
    "TV1: ${text_view_1.width}px, " +
    "TV2: ${text_view_2.width}px"
    )
    }
    }
    }

    View Slide

  87. class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    linear_layout.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
    trace(
    "LL: ${linear_layout.width}px, " +
    "TV1: ${text_view_1.width}px, " +
    "TV2: ${text_view_2.width}px"
    )
    }
    }
    }
    B LL: 500px, TV1: 300px, TV2:
    200px
    C LL: 400px, TV1: 233px, TV2:
    167px
    D LL: 400px, TV1: 300px, TV2:
    200px
    A LL: 400px, TV1: 267px, TV2:
    133px
    layout/activity_main.xml

    android:id="@+id/linear_layout"
    android:layout_width="400px"
    android:layout_height="match_parent">
    android:id="@+id/text_view_1"
    android:layout_width="300px"
    android:layout_height="match_parent"
    android:layout_weight="1" />
    android:id="@+id/text_view_2"
    android:layout_width="200px"
    android:layout_height="match_parent"
    android:layout_weight="2" />

    MainActivity.kt

    View Slide

  88. class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    linear_layout.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
    trace(
    "LL: ${linear_layout.width}px, " +
    "TV1: ${text_view_1.width}px, " +
    "TV2: ${text_view_2.width}px"
    )
    }
    }
    }
    B LL: 500px, TV1: 300px, TV2:
    200px
    C LL: 400px, TV1: 233px, TV2:
    167px
    D LL: 400px, TV1: 300px, TV2:
    200px
    A LL: 400px, TV1: 267px, TV2:
    133px
    layout/activity_main.xml

    android:id="@+id/linear_layout"
    android:layout_width="400px"
    android:layout_height="match_parent">
    android:id="@+id/text_view_1"
    android:layout_width="300px"
    android:layout_height="match_parent"
    android:layout_weight="1" />
    android:id="@+id/text_view_2"
    android:layout_width="200px"
    android:layout_height="match_parent"
    android:layout_weight="2" />

    MainActivity.kt

    View Slide

  89. View Slide

  90. So we might end
    up in some very
    extreme & weird
    cases…

    View Slide

  91. Locale resolution

    View Slide

  92. build.gradle
    apply from: puzzler
    android {
    defaultConfig {
    resConfigs "es"
    }
    }

    View Slide

  93. MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // User’s locale preferences are set to: en_US, fr_FR, es_ES
    trace("${LocaleList.getDefault()}, " +
    "${Locale.getDefault()}, ${text_view.text}"
    )
    }
    }

    View Slide

  94. MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // User’s locale preferences are set to: en_US, fr_FR, es_ES
    trace("${LocaleList.getDefault()}, " +
    "${Locale.getDefault()}, ${text_view.text}"
    )
    }
    }
    layout/activity_main.xml

    android:id="@+id/text_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:text="@string/hello_world" />

    View Slide

  95. MainActivity.kt
    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // User’s locale preferences are set to: en_US, fr_FR, es_ES
    trace("${LocaleList.getDefault()}, " +
    "${Locale.getDefault()}, ${text_view.text}"
    )
    }
    }
    layout/activity_main.xml

    android:id="@+id/text_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:text="@string/hello_world" />
    values/strings.xml

    Bonjour monde !

    View Slide

  96. MainActivity.kt
    layout/activity_main.xml

    android:id="@+id/text_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:text="@string/hello_world" />
    values/strings.xml

    Bonjour monde !

    values-es/strings.xml

    ¡Hola mundo!

    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // User’s locale preferences are set to: en_US, fr_FR, es_ES
    trace("${LocaleList.getDefault()}, " +
    "${Locale.getDefault()}, ${text_view.text}"
    )
    }
    }

    View Slide

  97. B [en_US,fr_FR,es_ES], fr_FR,
    Bonjour monde !
    C [en_US,fr_FR,es_ES], en_US,
    Bonjour monde !
    D Resources.NotFoundException
    A [fr,es], fr, Bonjour monde !
    MainActivity.kt
    layout/activity_main.xml

    android:id="@+id/text_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:text="@string/hello_world" />
    values/strings.xml

    Bonjour monde !

    values-es/strings.xml

    ¡Hola mundo!

    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // User’s locale preferences are set to: en_US, fr_FR, es_ES
    trace("${LocaleList.getDefault()}, " +
    "${Locale.getDefault()}, ${text_view.text}"
    )
    }
    }

    View Slide

  98. B [en_US,fr_FR,es_ES], fr_FR,
    Bonjour monde !
    C [en_US,fr_FR,es_ES], en_US,
    Bonjour monde !
    D Resources.NotFoundException
    A [fr,es], fr, Bonjour monde !
    MainActivity.kt
    layout/activity_main.xml

    android:id="@+id/text_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:text="@string/hello_world" />
    values/strings.xml

    Bonjour monde !

    values-es/strings.xml

    ¡Hola mundo!

    class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // User’s locale preferences are set to: en_US, fr_FR, es_ES
    trace("${LocaleList.getDefault()}, " +
    "${Locale.getDefault()}, ${text_view.text}"
    )
    }
    }

    View Slide

  99. User settings
    de_DE
    fr_FR
    es_ES
    App resources
    default
    es

    View Slide

  100. User settings
    de_DE
    fr_FR
    es_ES
    App resources
    default
    es
    Resolution path
    de
    de_DE
    Children of de
    fr_FR
    fr
    Children of fr
    es_ES
    es
    Children of es
    Default

    View Slide

  101. User settings
    de_DE
    fr_FR
    es_ES
    App resources
    default
    es
    Resolution path
    de
    de_DE
    Children of de
    fr_FR
    fr
    Children of fr
    es_ES
    es
    Children of es
    Default

    View Slide

  102. User settings
    de_DE
    fr_FR
    es_ES
    App resources
    default
    es
    Resolution path
    de
    de_DE
    Children of de
    fr_FR
    fr
    Children of fr
    es_ES
    es
    Children of es
    Default

    View Slide

  103. User settings
    de_DE
    fr_FR
    es_ES
    App resources
    default
    es
    Resolution path
    de
    de_DE
    Children of de
    fr_FR
    fr
    Children of fr
    es_ES
    es
    Children of es
    Default

    View Slide

  104. User settings
    de_DE
    fr_FR
    es_ES
    App resources
    default
    es
    Resolution path
    de
    de_DE
    Children of de
    fr_FR
    fr
    Children of fr
    es_ES
    es
    Children of es
    Default
    es

    View Slide

  105. User settings
    en_US
    fr_FR
    es_ES
    App resources
    default
    es
    Resolution path
    English is always considered as supported if in user settings

    View Slide

  106. User settings
    en_US
    fr_FR
    es_ES
    App resources
    default
    es
    Resolution path
    en
    en_US
    Children of en
    fr_FR
    fr
    Children of fr
    es_ES
    es
    Children of es
    Default
    English is always considered as supported if in user settings

    View Slide

  107. User settings
    en_US
    fr_FR
    es_ES
    App resources
    default
    es
    Resolution path
    en
    en_US
    Children of en
    fr_FR
    fr
    Children of fr
    es_ES
    es
    Children of es
    Default
    English is always considered as supported if in user settings

    View Slide

  108. User settings
    en_US
    fr_FR
    es_ES
    App resources
    default
    es
    Resolution path
    en
    en_US
    Children of en
    fr_FR
    fr
    Children of fr
    es_ES
    es
    Children of es
    Default
    English is always considered as supported if in user settings
    default

    View Slide

  109. That’s all folks!
    @cyrilmottier • cyrilmottier.com

    View Slide