===================[ Readings ]=================== Best explanation of TaskStackBuilder & entering apps from a Notification: https://developer.android.com/training/implementing-navigation/temporal.html#CreateBackStack You might wonder why notifications involve so many code examples (we've been mulling over at least three so far). The answer is that notifications are actually a sort of mini-apps, with many rendering options specific only to them. For example, Notifications can be rendered in several styles, the normal (as shown in the "drawer" list you get when you pull down the notification bar) and "big view" (when a notification is on top of the drawer or gets clicked): https://developer.android.com/training/notify-user/expanded.html Notifications can also include their own progress bars: https://developer.android.com/training/notify-user/display-progress.html This is also why Notification objects are not created directly (as they used to be in the early Android, see link below), but are constructed using the NotificationBuilder class, to manage complexity. This programming style is known as the Builder design pattern (https://en.wikipedia.org/wiki/Builder_pattern). Its purpose is to encapsulate the details of constructing complex objects. This stands to reason: notifications are a key entry point for the apps, and so are at least as important as the Launcher, if not more. A history of Android notifications, with neat graphical summaries of the newest features: https://www.objc.io/issues/11-android/android-notifications/ ===================[ Notification as an app entry point ]================== In Android user experience (UX), a Notification often serves as an entry point to the app, an alternative to being started from the Launcher. This is an important distinction: when users start an app by clicking on its icon in the Launcher, they act on their own initiative; by contrast, when the app designer wants the user to engage with the app in response to some event, the initiative belongs to the app. So the app typically guides the user not to the main activity, but to where some action must be taken---a different entry point in the app. Google Play update-nagging notificiations are one example (and now you know how they are generated). Another example would be a messaging or email app that spawns a service to poll for incoming messages and notify the user when one arrives. So Notification objects wrap an Intent that will be sent to start the app at the desired activity. This intent is further packaged in a PendingIntent object, which encapsulates the context (including permissions) that the app should receive when started (recall that the code that handles the click on the notification and sends the intent is not your code, but whatever handles the notification bar; your app or service that sent the notification may no longer be running when the user acts on the notification.) But there's one problem: after the notification is clicked, the Intent is sent, and starts the entry-point activity, what if the user clicks the Back button? Normally, clicking "back" in a detail-level activity of an app brings one back to the main activity of that app, from where the detailed view was called. But there was no main screen prior to the notification being processed: the current detail-level activity was not created from the main screen but from the notification! Nevertheless, in a messaging app, the user expects to go back to the list of messages after handling the newly arrived message; similarly for emails. So there must be a away to create the backstack of activities on demand, to keep the same work flow regardless of whether the app was started from the Launcher or Notification. This is what the TaskStack does: it allows you to prepare the expected backstack of activities in the PendingIntent, so that the Back button behaves as expected. Namely, in the Manifest you specify what parent your detail-level activity has, and then in the code you tell a TaskStackBuilder to build the stack of activities starting with DetailActivity backward: val resultIntent = Intent(this, DetailActivity::class.java) val stackBuilder : TaskStackBuilder = TaskStackBuilder.create(this) stackBuilder.addNextIntentWithParentStack(resultIntent) var resultPendingIntent : PendingIntent = stackBuilder.getPendingIntent(0, // 0 causes bugs on some systems! PendingIntent.FLAG_UPDATE_CURRENT) and then resultPendingIntent is added to the Notification being built by mBuilder via mBuilder.setContentIntent(resultPendingIntent) . The one-liner stackBuilder.addNextIntentWithParentStack(resultIntent) is equivalent to stackBuilder.addParentStack(DetailActivity::class.java) stackBuilder.addNextIntent(resultIntent) This is not very inuitive---you'd think addParentStack(..) would get the MainActivity as argument. But, AFAIK, what this call really does is initiates a look-up in the manifest, from DetailActivity back down its chain of parents. The code from class is in examples/NotiTaskStack; look at the comments. Read: https://developer.android.com/training/implementing-navigation/temporal.html#CreateBackStack this subject is also discussed in https://developer.android.com/training/notify-user/navigation.html but, unlike the above, I found it rather unclear. A discussion of the same topic on StackOverflow: https://stackoverflow.com/questions/36912325/why-do-we-use-the-taskstackbuilder For the recommended UI navigation patterns, see https://developer.android.com/design/patterns/navigation.html ==================[ Kotlin & the "Billion-dollar mistake" ]================== Above code examples favor long chains of method calls on an object such as the NotificationBuilder or TaskStackBuilder. The code looks like a long pipeline of calls: obj.method1().method2().method3().method4() This pipelining is convenient because you don't need to assign the intermediate results to any variables; you just use them once to call the next method. However, this pipelining style has two problems: 1. What if one of the methods returns a null? The next method call will fail with a NullPointerException. 2. Supposing one of the methods encounters an error and needs to return a special value indicating that. What about the rest of the chain? ---------------[ The problem of nulls ]--------------- The problem of nulls is known as the "Billion-dollar mistake" (cf. https://en.wikipedia.org/wiki/Tony_Hoare), allowing a null reference to be a valid value of any type. (A funny blogpost about this: https://www.lucidchart.com/techblog/2015/08/31/the-worst-mistake-of-computer-science/) Kotlin addresses this problem radically: unlike Java, null is NOT allowed to be a valid value of a type. For example, a variable of the type Bundle or String cannot be null. Assigning a null to such a variable will fail to type-check (at compile time), saving you from a NullPointerException at runtime. If you want a null to be allowed, or need to catch a value that could be null (like the Bundle argument of an onCreate(..) on the first invocation of an Activity), you must declare it as a different type, Bundle? . This type is not compatible with Bundle: as assignment of a Bundle? typed variable to a Bundle-typed one will fail (so that you can confidently use Bundle-typed values, knowing they never get a null assigned to them). So a Kotlin pipeline is safe if the participating object types don't have a "?". If some do, you can use the safe derefence operator, ?. This creation of an additional, null-allowing type for every type of object may seem like a huge nuisance rife with type casts all around the code. But Kotlin saves the day by making "smart casts" a core part of the language: if you use a Bundle? object under a condition that excludes null, it is treated as compatible with Bundle, and not in need of a type cast! E.g., override fun onCreate( savedInstanceState : Bundle? ){ var b : Bundle b = savedInstanceState // error, fail to type-check! if( savedInstanceState != null ) b = savedInstanceState // OK, smart cast } The smart cast means that the Kotlin compiler (and Android Studio) must incorporate static analyzers that reason about whether a null would be possible to get inside a block given the preceding conditionals. This is quite amazing---and happens in the Studio *as you type*. This is the future of programming languages. See https://kotlinlang.org/docs/reference/null-safety.html A good blog discussion: https://medium.com/@trevorwang/kotlin-null-safety-15070571f6a8 You will enjoy the "Elvis operator", ?: :) ---------------[ Dealing with multiple result types ]--------------- The second problem is more subtle. Many projects make the mistake of overloading some valid value within the return type of their API to mean that something went wrong. A sterling example of how this can work out in the real world comes from how the MaxMind geolocating company handled the "we only know this IP is in the continental US" condition: https://www.washingtonpost.com/news/morning-mix/wp/2016/08/10/lawsuit-how-a-quiet-kansas-home-wound-up-with-600-million-ip-addresses-and-a-world-of-trouble/ The solution to this problem is to be able to construct types on demand, out of other types, as "a value of this type can be a value of any of these several types". This is called a _union type_, and is an instance of what is called ADTs, Algebraic Data Types. Algebraic refers to the fact that types can be constructed out of other types, just as logical formulas can be constructed in Boolean algebra with AND and OR, or sets can be constructed of other sets with UNION and INTERSECTION operations. So Bundle?, String?, and other such null-allowing Kotlin types are union types, made as a union of non-nullable objects and null. MaxMind could have designed their API to return a union type of actual geolocation coordinates and an Error object that would explain the nature of the failure and how to interpret the result. The last piece of the puzzle is what a pipeline should do when it finds the special value, like null or Error. The winning recipe is to simply pass it along: null gives a null and safely travels down the pipeline and the result is null. The code for that can be a part of the operator such as Kotlin's "?.", so that the programmer can focus on the real values. ===== to be continued ====