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. Small applications with curious behavior Android puzzlers 1. What does

    it print? 2. The solution revealed 3. The moral
  2. 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
  3. <?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
  4. 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
  5. <?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}") } }
  6. <?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}") } }
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. attaches child to parent & returns parent null null parent

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

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

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

    root attachToRoot returns child returns child attaches child to parent & returns parent Output
  16. 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}" ) } }
  17. 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
  18. 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}" ) } }
  19. 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>
  20. <?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
  21. 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() } }
  22. 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() } }
  23. 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() } }
  24. 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(", ")) } }
  25. 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(", ")) } }
  26. 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(", ")) } }
  27. 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(", ")) } }
  28. 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(", ")) } }
  29. 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(", ")) } }
  30. 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
  31. Exceptions <selector /> <animated-selector /> <layer-list /> <animation-list /> <shape

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

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

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

    /> <animated-rotate /> <com.cyrilmottier.YourDrawable /> StateListDrawable AnimatedStateListDrawable LayerDrawable AnimationDrawable GradientDrawable ShapeDrawable PaintDrawable PictureDrawable YourDrawable
  35. 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() } }
  36. 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 } }
  37. 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 } }
  38. 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 } }
  39. 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
  40. 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)}") } } }
  41. 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
  42. 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)}") } } }
  43. It rather “hides”: the only possible deletion is app uninstall

    Doesn’t really delete fun NotificationManager .deleteNotificationChannel( channelId: String )
  44. 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.
  45. 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.
  46. 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.
  47. 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!") } }
  48. 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!") } }
  49. 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!") } }
  50. 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" ) } } }
  51. 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" ) } } }
  52. 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
  53. 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
  54. 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}" ) } }
  55. 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" />
  56. 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>
  57. 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}" ) } }
  58. 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}" ) } }
  59. 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}" ) } }
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. User settings en_US fr_FR es_ES App resources default es Resolution

    path English is always considered as supported if in user settings
  66. 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
  67. 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
  68. 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