===================[ 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 ====