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 full-size slide

  2. It all began with…

    View full-size slide

  3. Small applications with
    curious behavior
    Android puzzlers

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. 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 full-size slide


  8. 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 full-size slide

  9. 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 full-size slide

  10. Inflation result

    View full-size slide


  11. 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 full-size 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 full-size slide

  13. 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 full-size slide

  14. 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 full-size slide

  15. 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 full-size slide

  16. 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 full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

  22. Padding precedence

    View full-size slide

  23. 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 full-size 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}"
    )
    }
    }

    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 full-size slide

  25. 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 full-size slide

  26. 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 full-size slide



  27. 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 full-size slide

  28. background Drawable padding
    supportsRtl = false
    Lower
    Lower
    Higher

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  33. 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 full-size slide

  34. Activity lifecycle

    View full-size slide

  35. 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 full-size slide

  36. 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 full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  40. Drawable tags

    View full-size slide

  41. 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 full-size slide

  42. 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 full-size slide

  43. 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 full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. The general rule
    ValarMorghulisDrawable

    View full-size slide

  48. The general rule

    View full-size slide

  49. The general rule













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

    View full-size slide

  50. Exceptions
    The general rule

    View full-size slide

  51. Exceptions





    StateListDrawable
    AnimatedStateListDrawable
    LayerDrawable
    AnimationDrawable
    GradientDrawable

    View full-size slide

  52. Exceptions





    StateListDrawable
    AnimatedStateListDrawable
    LayerDrawable
    AnimationDrawable
    GradientDrawable
    ShapeDrawable
    PaintDrawable
    PictureDrawable

    View full-size slide

  53. Exceptions






    StateListDrawable
    AnimatedStateListDrawable
    LayerDrawable
    AnimationDrawable
    GradientDrawable
    ShapeDrawable
    PaintDrawable
    PictureDrawable

    View full-size slide

  54. Exceptions







    StateListDrawable
    AnimatedStateListDrawable
    LayerDrawable
    AnimationDrawable
    GradientDrawable
    ShapeDrawable
    PaintDrawable
    PictureDrawable
    YourDrawable

    View full-size slide

  55. Fragment lifecycle

    View full-size slide

  56. 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 full-size slide

  57. 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 full-size slide

  58. 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 full-size slide

  59. 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 full-size slide

  60. 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 full-size slide

  61. Notification channels

    View full-size slide

  62. 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 full-size slide

  63. 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 full-size slide

  64. 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 full-size slide

  65. fun NotificationManager
    .deleteNotificationChannel(
    channelId: String
    )

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  68. 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 full-size slide

  69. 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 full-size slide

  70. 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 full-size slide

  71. Preferences read/write

    View full-size slide

  72. 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 full-size slide

  73. 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 full-size slide

  74. 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 full-size slide

  75. apply() is atomic
    async to disk

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  81. LinearLayout weight

    View full-size slide

  82. 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 full-size slide

  83. 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 full-size slide

  84. 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 full-size slide

  85. 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 full-size slide

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

    View full-size slide

  87. Locale resolution

    View full-size slide

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

    View full-size slide

  89. 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 full-size slide

  90. 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 full-size slide

  91. 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 full-size slide

  92. 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 full-size slide

  93. 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 full-size slide

  94. 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 full-size slide

  95. User settings
    de_DE
    fr_FR
    es_ES
    App resources
    default
    es

    View full-size slide

  96. 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 full-size slide

  97. 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 full-size slide

  98. 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 full-size slide

  99. 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 full-size 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
    es

    View full-size slide

  101. 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 full-size slide

  102. 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 full-size slide

  103. 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 full-size slide

  104. 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 full-size slide

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

    View full-size slide