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.

E9bf8f6d5480ea2a2623df7dccfd1f70?s=128

Cyril Mottier

April 24, 2019
Tweet

Transcript

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

    ? ? ? ? ? ? ?
  2. It all began with…

  3. None
  4. Small applications with curious behavior Android puzzlers

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

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

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

    it print? 2. The solution revealed 3. The moral
  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
  9. <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.cyrilmottier.android.puzzlers" tools:ignore="AllowBackup,GoogleAppIndexingWarning"> <application android:allowBackup=“false"

    android:label=“@string/app_name“ android:supportsRtl="true" android:theme="@style/android:Theme.Material.Light.DarkActionBar"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> A simple setup AndroidManifest.xml
  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
  11. Inflation result

  12. <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" 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}") } }
  13. <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" 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}") } }
  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}") } } <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:tag="child" /> MainActivity.kt layout/view_inflate.xml
  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}") } } <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:tag="child" /> MainActivity.kt layout/view_inflate.xml
  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}") } } <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:tag="child" /> MainActivity.kt layout/view_inflate.xml
  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}") } } <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:tag="child" /> MainActivity.kt layout/view_inflate.xml
  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}") } } <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:tag="child" /> MainActivity.kt layout/view_inflate.xml
  19. attaches child to parent & returns parent null null parent

    parent returns child root returns child attaches child to parent & returns parent Output
  20. Favor the 3-args inflate method instead fun LayoutInflater.inflate( resource: Int,

    root: ViewGroup, attachToRoot: Boolean ): View Be explicit
  21. null null parent parent false true false true returns child

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

    root attachToRoot returns child returns child attaches child to parent & returns parent Output
  23. Padding precedence

  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}" ) } }
  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}" ) } } <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" 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
  26. MainActivity.kt <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" 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"/> <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <padding android:bottom="1px" android:left="2px" android:right="3px" android:top="4px" /> </shape> 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}" ) } }
  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 <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" 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 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <padding android:bottom="1px" android:left="2px" android:right="3px" android:top="4px" /> </shape>
  28. <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <padding android:bottom="1px" android:left="2px" android:right="3px" android:top="4px"

    /> </shape> 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}" ) } } <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" 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
  29. background Drawable padding supportsRtl = false Lower Lower Higher

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

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

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

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

    supportsRtl = false
  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
  35. Activity lifecycle

  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() } }
  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() } }
  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() } }
  39. None
  40. “The following diagram shows the important state paths of an

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

    Activity. – d.android.com
  42. Drawable tags

  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(", ")) } }
  44. MainActivity.kt drawable/level_list.xml <?xml version="1.0" encoding="utf-8"?> <level-list /> 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(", ")) } }
  45. MainActivity.kt drawable/level_list.xml <?xml version="1.0" encoding="utf-8"?> <level-list /> drawable/layer_list.xml <?xml version="1.0"

    encoding="utf-8"?> <layer-list /> 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(", ")) } }
  46. MainActivity.kt drawable/level_list.xml <?xml version="1.0" encoding="utf-8"?> <level-list /> drawable/layer_list.xml <?xml version="1.0"

    encoding="utf-8"?> <layer-list /> drawable/shape.xml <?xml version="1.0" encoding="utf-8"?> <shape /> 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(", ")) } }
  47. B LevelDrawable, LayerListDrawable, ShapeDrawable C LevelListDrawable, LayerDrawable, GradientDrawable D throws

    InflateException A LevelListDrawable, LayerListDrawable, ShapeDrawable MainActivity.kt drawable/level_list.xml <?xml version="1.0" encoding="utf-8"?> <level-list /> drawable/layer_list.xml <?xml version="1.0" encoding="utf-8"?> <layer-list /> drawable/shape.xml <?xml version="1.0" encoding="utf-8"?> <shape /> 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(", ")) } }
  48. B LevelDrawable, LayerListDrawable, ShapeDrawable C LevelListDrawable, LayerDrawable, GradientDrawable D throws

    InflateException A LevelListDrawable, LayerListDrawable, ShapeDrawable MainActivity.kt drawable/level_list.xml <?xml version="1.0" encoding="utf-8"?> <level-list /> drawable/layer_list.xml <?xml version="1.0" encoding="utf-8"?> <layer-list /> drawable/shape.xml <?xml version="1.0" encoding="utf-8"?> <shape /> 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(", ")) } }
  49. The general rule <valar-morghulis /> ValarMorghulisDrawable

  50. The general rule

  51. The general rule <level-list /> <transition /> <ripple /> <adaptive-icon

    /> <color /> <vector /> <animated-vector /> <scale /> <clip /> <rotate /> <inset /> <bitmap /> <nine-patch /> LevelListDrawable TransitionDrawable RippleDrawable AdaptiveIconDrawable ColorDrawable VectorDrawable AnimatedVectorDrawable ScaleDrawable ClipDrawable RotateDrawable InsetDrawable BitmapDrawable NinePatchDrawable <animated-image /> AnimatedImageDrawable xceptions
  52. Exceptions The general rule

  53. Exceptions <selector /> <animated-selector /> <layer-list /> <animation-list /> <shape

    /> StateListDrawable AnimatedStateListDrawable LayerDrawable AnimationDrawable GradientDrawable
  54. Exceptions <selector /> <animated-selector /> <layer-list /> <animation-list /> <shape

    /> StateListDrawable AnimatedStateListDrawable LayerDrawable AnimationDrawable GradientDrawable ShapeDrawable PaintDrawable PictureDrawable
  55. Exceptions <selector /> <animated-selector /> <layer-list /> <animation-list /> <shape

    /> <animated-rotate /> StateListDrawable AnimatedStateListDrawable LayerDrawable AnimationDrawable GradientDrawable ShapeDrawable PaintDrawable PictureDrawable
  56. Exceptions <selector /> <animated-selector /> <layer-list /> <animation-list /> <shape

    /> <animated-rotate /> <com.cyrilmottier.YourDrawable /> StateListDrawable AnimatedStateListDrawable LayerDrawable AnimationDrawable GradientDrawable ShapeDrawable PaintDrawable PictureDrawable YourDrawable
  57. Fragment lifecycle

  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() } }
  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 } }
  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 } }
  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 } }
  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
  63. Notification channels

  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)}") } } }
  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
  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)}") } } }
  67. fun NotificationManager .deleteNotificationChannel( channelId: String )

  68. It rather “hides”: the only possible deletion is app uninstall

    Doesn’t really delete fun NotificationManager .deleteNotificationChannel( channelId: String )
  69. Name/description Always modifiable Mainly for i18n purposes & user-defined names/descriptions.

  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.
  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.
  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.
  73. Preferences read/write

  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!") } }
  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!") } }
  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!") } }
  77. apply() is atomic async to disk

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

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

  80. None
  81. “ defValue - String: Value to return if this preference

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

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

    “defValue”
  84. LinearLayout weight

  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" ) } } }
  86. layout/activity_main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout" android:layout_width="400px" android:layout_height="match_parent"> <TextView

    android:id="@+id/text_view_1" android:layout_width="300px" android:layout_height="match_parent" android:layout_weight="1" /> <TextView android:id="@+id/text_view_2" android:layout_width="200px" android:layout_height="match_parent" android:layout_weight="2" /> </LinearLayout> 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" ) } } }
  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 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout" android:layout_width="400px" android:layout_height="match_parent"> <TextView android:id="@+id/text_view_1" android:layout_width="300px" android:layout_height="match_parent" android:layout_weight="1" /> <TextView android:id="@+id/text_view_2" android:layout_width="200px" android:layout_height="match_parent" android:layout_weight="2" /> </LinearLayout> MainActivity.kt
  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 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout" android:layout_width="400px" android:layout_height="match_parent"> <TextView android:id="@+id/text_view_1" android:layout_width="300px" android:layout_height="match_parent" android:layout_weight="1" /> <TextView android:id="@+id/text_view_2" android:layout_width="200px" android:layout_height="match_parent" android:layout_weight="2" /> </LinearLayout> MainActivity.kt
  89. None
  90. So we might end up in some very extreme &

    weird cases…
  91. Locale resolution

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

    } }
  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}" ) } }
  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 <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/text_view" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="@string/hello_world" />
  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 <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" 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 <resources> <string name="hello_world">Bonjour monde !</string> </resources>
  96. MainActivity.kt layout/activity_main.xml <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" 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 <resources> <string name="hello_world">Bonjour monde !</string> </resources> values-es/strings.xml <resources> <string name="hello_world">¡Hola mundo!</string> </resources> 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}" ) } }
  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 <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" 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 <resources> <string name="hello_world">Bonjour monde !</string> </resources> values-es/strings.xml <resources> <string name="hello_world">¡Hola mundo!</string> </resources> 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}" ) } }
  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 <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" 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 <resources> <string name="hello_world">Bonjour monde !</string> </resources> values-es/strings.xml <resources> <string name="hello_world">¡Hola mundo!</string> </resources> 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}" ) } }
  99. User settings de_DE fr_FR es_ES App resources default es

  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
  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
  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
  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
  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
  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
  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
  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
  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
  109. That’s all folks! @cyrilmottier • cyrilmottier.com