Ohai! You've just joined us for the second part of a blog series about the Play Services Task API, which is used by some Firebase features to respond to work that its APIs perform asynchronously. Last time, we got acquainted with a Task used by the Firebase Storage API, and learned a little bit about how Tasks work in general. So, if you haven't seen that post, now's good time to circle back to it before continuing here. In this post, we'll take a look at some of the nuances in behavior between the different variations for adding a listener to a Task to capture its result.
Last time, we saw a listener get added to a Task like this, using the Firebase Storage API:
Task task = forestRef.getMetadata(); task.addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(StorageMetadata storageMetadata) { // Metadata now contains the metadata for 'images/forest.jpg' } });
In this code, addOnSuccessListener() is called with a single argument, which is an anonymous listener to invoke upon completion. With this form, the listener is invoked on the main thread, which means we can do things that can only be done on the main thread, such as update a View. It's great that the Task helps put the work back on the main thread, except there is one caveat here. If a listener is registered like this in an Activity, and it's not removed before the Activity is destroyed, there is a possibility of an Activity leak.
Right, nobody wants leaky Activities! So, what's an Activity leak, anyway? Put briefly, an Activity leak occurs when an object holds onto an Activity object reference beyond its onDestroy() lifecycle method, retaining the Activity beyond its useful lifetime. When onDestroy() is called on an Activity, you can be certain that instance is never going to be used by Android again. After onDestroy(), we want the Android runtime garbage collector to clean up that Activity, all of its Views, other dead objects. But the garbage collector won't clean up the Activity and all of its Views if some other object is holding a strong reference to it!
Activity leaks can be a problem with Tasks, unless you take care to avoid it. In the above code (if it was inside an Activity), the anonymous listener object actually holds a strong, implicit reference to the containing Activity. This is how code inside the listener is able to make changes to the Activity and its members - the compiler silently works out the details of that. An Activity leak occurs when an in-progress Task holds on to the listener past the Activity's onDestroy(). We really don't have any guarantees at all about how long that Task will take, so the listener can be held indefinitely. And since the listener implicitly holds a reference to the Activity, the Activity can be leaked if the Task doesn't complete before onDestroy(). If lots of Tasks holding references to Activities back up over time (for example, due to a hung network), that can cause your app to run out of memory and crash. Yow. You can learn more in this video.
If you’re concerned about leaking Activities (and I hope you are!), you should know that the single argument version of addOnSuccessListener() has the caveat of possibly leaking the Activity if you aren't careful to remove the listener at the right time.
It turns out there's a convenient way to do this automatically with the Task API. Let's take the above code in an Activity, and modify its call to addOnSuccessListener() slightly:
Task task = forestRef.getMetadata(); task.addOnSuccessListener(this, new OnSuccessListener() { @Override public void onSuccess(StorageMetadata storageMetadata) { // Metadata now contains the metadata for 'images/forest.jpg' } });
This is exactly like the previous version, except there are now two arguments to addOnSuccessListener(). The first argument is `this`, so when this code is in an Activity, that would make `this` refer to that enclosing Activity instance. When the first parameter is an Activity reference, that tells the Task API that this listener should be "scoped" to the lifecycle of the Activity. This means that the listener will be automatically removed from the task when the Activity goes through its onStop() lifecycle method. This is pretty handy because you don't have to remember to do it yourself for all the Tasks you may create while an Activity active. However, you need to be confident that onStop() is the right place for you to stop listening. onStop() is triggered when an Activity is no longer visible, which is often OK. However, if you intend to keep tracking the Task in the next Activity (such as when an orientation change replaces the current Activity with a new one), you'll need to come up with a way to retain that knowledge in the next Activity. For some information on that, read up on saving Activity state.
There are cases where you simply don't want to react to the completion of a Task on the main thread. Maybe you want to do blocking work in your listener, or you want to be able to handle different Task results concurrently (instead of sequentially). So, you'd like to avoid the main thread altogether and instead process the result on another thread you control. There's one more form of addOnSuccessListener() that can help your app with your threading. It looks like this (with abbreviated listener):
Executor executor = ...; // obtain some Executor instance Task task = RemoteConfig.getInstance().fetch(); task.addOnSuccessListener(executor, new OnSuccessListener() { ... });
Here, we're making a call to the Firebase Remote Config API to fetch new configuration values. Then, the returned Task from fetch() gets a call to addOnSuccessListener() and receives an Executor as the first argument. This Executor determines the thread that will be used to invoke the listener. For those of you unfamiliar with Executor, it's a core Java utility that accepts units of work and routes them to be executed on threads under its control. That could be a single thread, or a pool of threads, all waiting to do work. It's not very common to use for apps to use an Executor directly, and can be seen as an advanced technique for managing the threading behavior of your app. What you should take away from this is the fact that you don't have to receive your listeners on the main thread if that doesn't suit your situation. If you do choose to use an Executor, be sure to manage them as shared singletons, or make sure their lifecycles are managed well so you don’t leak their threads.
One other interesting thing to note about this code is the fact that the Task returned by Remote Config is parameterized by Void. This is the way a Task can say that it doesn't generate any object directly - Void is the data type in Java that indicates absence of type data. The Remote Config API is simply using the Task as an indicator of task completion, and the caller is expected to use other Remote Config APIs to discover any new values that were fetched.
All told, there are three varieties of addOnSuccessListener():
Task addOnSuccessListener(OnCompleteListener listener) Task addOnSuccessListener(Activity activity, OnSuccessListener listener) Task addOnSuccessListener(Executor executor, OnSuccessListener listener)
On top of that, we have the same varieties for failure and completion listeners:
Task addOnFailureListener(OnFailureListener listener) Task addOnFailureListener(Activity activity, OnFailureListener listener) Task addOnFailureListener(Executor executor, OnFailureListener listener) Task addOnCompleteListener(OnCompleteListener listener) Task addOnCompleteListener(Activity activity, OnCompleteListener listener) Task addOnCompleteListener(Executor executor, OnCompleteListener listener)
There's nothing too special going on with OnCompleteListener. It's just a single listener that's capable of receiving both success and failure, and you have to check for that status inside the callback. The file metadata callback from the last post could be rewritten like this, instead of giving the task separate success and failure listeners:
Task task = forestRef.getMetadata(); task.addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete (Task task) { if (task.isSuccessful()) { StorageMetadata meta = task.getResult(); // Do something with metadata... } else { Exception e = task.getException(); // Handle the failure... } } });
So, with OnCompleteListener, you can have a single listener that handles both success and failure, and you find out which one by calling isSuccessful() on the Task object passed to the callback. Practically speaking, this is functionally equivalent to registering both an OnSuccessListener and an OnFailureListener. The style you choose is mostly a matter of preference.
Now you've seen that Tasks can receive three different kinds of listeners: success, failure, and overall completion. And, for each of those kinds of listeners, there are three ways to receive that callback: on the main thread, on the main thread scoped to an Activity, and on a thread determined by an Executor. You have some choices here, and it's up to you to choose which one suits your situation the best. However, these aren't the only ways to handle the results of your Tasks. You can create pipelines of Task results for more complex processing. Please join me for those details next time, where you can continue the journey to become a Firebase Taskmaster!
If you have any questions, consider using Twitter with the #AskFirebase hashtag or the firebase-talk Google Group. We also have a dedicated Firebase Slack channel. And you can follow me @CodingDoug on Twitter.
We’re excited to announce that the registration for the Firebase Dev Summit is opening today!
Six months ago, thousands of developers joined us at Google I/O in Mountain View, CA to hear about the expansion of Firebase to become a unified app platform that helps developers build better apps and grow successful businesses. We want to share these updates with you (and maybe even a few new ones!) at the Firebase Dev Summit in Berlin, Germany. Registration is now open, but keep in mind that space will be filled on a first-come, first-serve basis, so make sure to register today.
Our product managers and engineering team (including me!) will be there, and we’re excited to meet you in person and learn how we can make Firebase easier for you to develop extraordinary experiences for your users on iOS, Android, and the Web.
The Firebase Dev Summit is full day event for app developers that will focus on how to use Firebase with your apps. The day will have a packed agenda with valuable sessions from Firebase and our partners, and is a great chance to meet developers from your local community. But, the day isn’t just about us talking to to you -- we also want to see you get your hands dirty with Firebase. You’ll get a chance to put your new knowledge into practice with a hands-on workshop and codelabs that walk you through all the different features of Firebase. Firebase engineers will be on hand to help you get up and running, and answer any questions you may have.
We’re looking forward to meeting you in person. Danke!
Hey there, iOS Developers!
We wanted to let you know that Firebase version 3.6 is now available for iOS. This contains a number of important bug fixes and features needed for iOS 10 support, and we encourage you to run a pod update (or manually update your frameworks) and recompile your projects at your earliest convenience.
pod update
If you want to see a full list of fixes and improvements, you can review the release notes, but here's a quick summary of what's new.
Firebase Cloud Messaging now has support for the new iOS 10 user notifications. If your app is running on iOS 10, you can handle incoming notifications using the userNotificationCenter:willPresentNotification: withCompletionHandler method. And don't worry -- if your app only has the older application:didReceiveRemoteNotification: completionHandler methods supported, APNs will call those instead if it can't find the newer ones. Need more info? Refer to the updated FCM documentation for more information.
userNotificationCenter:willPresentNotification: withCompletionHandler
application:didReceiveRemoteNotification: completionHandler
With the iOS 10 update, Apple made a number of changes to their App Store review guidelines. The latest version of Firebase has made several changes in response to these new guidelines. Most importantly, you should no longer encounter iTunes Connect errors asking you to provide text for things like NSCalendarsUsageDescription and NSBluetoothPeripheralUsageDescription.
NSCalendarsUsageDescription
NSBluetoothPeripheralUsageDescription
One consequence of following these guidelines is that we have removed the technology which up until recently gave you the ability to measure iOS Search app install ads from Safari.
For those of you who are using Firebase Invites, you will need to supply some content for NSContactsUsageDescription in your plist file. Firebase Invites uses this contact information to populate the list of friends that your user might want to send an invitation to.
NSContactsUsageDescription
plist
Of course, this is an ongoing process. We will monitor the impact of these changes closely, and publish further updates if it ever becomes necessary.
You may recall in a recent blog post that Firebase Auth was encountering errors in Xcode 8 due to it not being able to write values to the keychain in the simulator. While that issue still exists, we have developed a workaround where we use NSUserDefaults in the simulator, and continue to use the keychain on the device. This means you can now develop and test out Firebase Auth in the simulator and everything should be working again.
NSUserDefaults
You found bugs; we fixed 'em! Please continue to report any issues or feature requests you might have to our online form, and we'll make sure they get handled appropriately.
And if you have any questions, you can always ask them on Stack Overflow with the Firebase tag, or send them to our Google group.
Thanks again for being a Firebase developer! Now go forth and update your apps!
Sometimes, when using the Firebase client APIs for Android, it's required that Firebase perform some work at the request of the developer in an asynchronous fashion. Perhaps some requested data is not immediately available, or work needs to be queued for eventual execution. When we say some work must be done asynchronously in an app, that means the work needs to happen at the same time as the app performs its primary job of rendering the app’s views, but not get in the way of that work. To perform this asynchronous work correctly in Android apps, the work can't occupy time on the Android main thread, otherwise the app may delay rendering of some frames, causing "jank" in the user experience, or worse, the dreaded ANR! Typical examples of work that can cause delays are network requests, reading and writing files, and lengthy computations. In general, we call this blocking work, and we never want to block the main thread!
When a developer uses a Firebase API to request work that would normally block the main thread, the API needs to arrange that work to run on a different thread, in order to avoid jank and ANRs. Upon completion, the results of that work sometimes have to make it back to the main thread in order to safely update views.
That's what the Play services Task API is for. The goal of the Task API is to provide an easy, lightweight, and Android-aware framework for Firebase (and Play services) client APIs to perform work asynchronously. It was introduced in Play services version 9.0.0 along with Firebase. If you've been using Firebase features in your app, it's possible that you may have been using the Task API without even realizing it! So, what I'd like to do in this blog series is unpack some of the ways the Firebase APIs make use of Tasks, and discuss some patterns for advanced use.
Before we begin, it's important to know that the Task API isn't a full replacement for other threading techniques on Android. The Android team has put together some great content that describe other tools for threading, such as Services, Loaders, and Handlers. There's also a whole season of Application Performance Patterns on YouTube that discusses your options. Some developers even opt into third party libraries that will help you with your threading in Android apps. So, it's up to you to learn about those and determine which solution is the best for your particular threading needs. Firebase APIs uniformly use Tasks to manage threaded work, and you can use those in conjunction with other strategies as you see fit.
If you're using Firebase Storage, you'll definitely encounter Tasks at some point. Here's a straightforward example of fetching metadata about a file that's already uploaded to Storage, taken directly from the documentation for file metadata:
// Create a storage reference from our app StorageReference storageRef = storage.getReferenceFromUrl("gs://"); // Get reference to the file StorageReference forestRef = storageRef.child("images/forest.jpg"); forestRef.getMetadata().addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(StorageMetadata storageMetadata) { // Metadata now contains the metadata for 'images/forest.jpg' } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception exception) { // Uh-oh, an error occurred! } });
Even though we never see a "Task" anywhere in this code, there is actually a Task in play here. The last part of the above code could be rewritten equivalently like this:
Task task = forestRef.getMetadata(); task.addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(StorageMetadata storageMetadata) { // Metadata now contains the metadata for 'images/forest.jpg' } }); task.addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception exception) { // Uh-oh, an error occurred! } });
Ah, it looks like there was a Task hidden in that code after all!
With the sample code rewritten above, it's now more clear how a Task is being used to obtain file metadata. The getMetadata() method on the StorageReference has to assume that the file metadata is not immediately available, so it will make a network request to get a hold of it. So, in order to avoid blocking the calling thread on that network access, getMetadata() returns a Task that can be listened to for eventual success or failure. The API then arranges to perform the request on a thread it controls. The details of this threading are hidden by the API, but the returned Task is used to indicate when the results become available. The returned Task then guarantees that any added listeners will be invoked upon completion. This form of API to manage the results of asynchronous work is sometimes called a Promise in other programming environments.
Notice here that the returned Task is parameterized by the type StorageMetadata, and that's also the type of object that gets passed to onSuccess() in the OnSuccessListener. In fact, all Tasks must declare a generic type in this way to indicate the type of data they generate, and the OnSuccessListener must share that generic type. Also, when an error occurs, an Exception is passed to onFailure() in the OnFailureListener, which will probably be the specific exception that caused the failure. If you want to know more about that Exception, you may have to check its type in order to safely cast it to the expected type.
The last thing to know about this code is that the listeners will be called on the main thread. The Task API arranges for this to happen automatically. So, if you want to do something in response to the StorageMetadata becoming available that must happen on the main thread, you can do that right there in the listener method. (But remember that you still shouldn’t be doing any blocking work in that listener on the main thread!) You have some options about how these listeners work, and I'll say more in a future post about your alternatives.
Some Firebase features provide other APIs that accept listeners that are not associated with Tasks. For example, if you're using Firebase Authentication, you've almost certainly registered a listener to find out when the user successfully logs in or out of your app:
private FirebaseAuth auth = FirebaseAuth.getInstance(); private FirebaseAuth.AuthStateListener authStateListener = new FirebaseAuth.AuthStateListener() { @Override public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { // Welcome! Or goodbye? } }; @Override protected void onStart() { super.onStart(); auth.addAuthStateListener(authStateListener); } @Override protected void onStop() { super.onStop(); auth.removeAuthStateListener(authStateListener); }
The FirebaseAuth client API makes two main guarantees for you here when you add a listener with addAuthStateListener(). First, it will call your listener immediately with the currently known login state for the user. Then, it will call the listener again with all subsequent changes to the user's login state, for as long as the listener is added to the FirebaseAuth object. This behavior is very different than the way Tasks work!
Tasks only call any added listener at most once, and only after the result is available. Also, the Task will invoke a listener immediately if the result was already available before that listener was added. The Task object effectively remembers the final result object and continues to deal it out to any future listeners, until it has no more listeners and is eventually garbage collected. So if you're using a Firebase API that works with listeners on something other than a Task object, be sure to understand its own behaviors and guarantees. Don't assume that all Firebase listeners behave like Task listeners!
Consider the active lifetime of your added Task listeners. There are two things that can go wrong if you don’t do this. First, you can cause an Activity leak if the Task continues beyond the lifetime of an Activity and its Views that are being referenced by an added listener. Second, the listener might execute when it’s no longer needed, causing wasteful work to be done, and possibly doing things that access Activity state when it’s no longer valid. The next part of this blog series will go into these issues in more detail, and how to avoid them.
We've taken a brief look at the Play Services Task API and uncovered its (sometimes hidden!) use in some Firebase sample code. Tasks are the way that Firebase lets you respond to work that has an unknown duration and must be executed off the main thread. Tasks can also arrange for listeners to be executed back on the main thread to deal with the result of the work. However, we've only just scratched the surface of what Tasks can do for you. Next time, we'll look at the variations on Task listeners so you can decide which one best suits your use cases.
In my last post I covered the five components under Pirate Metrics - Acquisition, Activation, Retention, Referral and Revenue - and their importance to the success of a product. In this post, I will focus on the first metric, i.e Acquisition, and demonstrate how one can use the Firebase suite to not only track, but also improve it.
At Google, our key offering over the years for acquisition has been Adwords. Through an Adwords campaign, you can already reach out to users on not only search results, but also places like YouTube and Google Play. With the new Firebase integration for Adwords, you can turbo charge your acquisition workflow even further.
Firstly, you can automatically ensure if your campaign are getting the right users by tracking the app open events being fired by them.
Say you have created a game, and have multiple campaigns running. Through this integration, you not only know which campaigns are bringing you more users at better rates, but also which ones are providing more engaged users.
You can also attribute acquisitions from more than 30 other networks, and track the campaign performances in Firebase Analytics directly. And, as you’d expect, you can segment users acquired from these different sources into dedicated Audiences.
Additionally, you can also specify which of your in-app events are important, and Adwords would automatically target those users likely to perform them. Continuing with the example of the game from before, let's assume your game has both single and multiplayer modes. Simply by letting Adwords know of the event for starting a multiplayer game, you could increase the likelihood of acquiring users who want to play multiplayer.
And, lastly, you can target the audiences you have created in Firebase Analytics. This can be tremendously powerful for retargeting, such as bringing back users who might quit after struggling at a certain level with a deal for a special power up.
The Firebase integration with Adwords helps you get the best bang for your buck. Do check out the official documentation for complete details.
Besides Adwords, another nifty tool that we provide as part of Firebase is Dynamic Links. Dynamic Links allows you to create a single URL to share with potential users, who would be redirected to the appropriate stores to download them on either Android or iOS. You can also add some custom data to a link, which will survive the app installation process. You could use this to considerably improve your acquisition from channels such as social media.
For example, say you want to highlight a product that is available for sale on your E-commerce application. Simply create a Dynamic Link, add some information such as an ID that your app can then consume and deeplink straight to the product. Users who have the app would be taken to the product page. Those who don’t would first be taken to either the Play Store or App Store, and can then be taken straight to the product when they open your application for the first time.
We’ll be covering more about Dynamic Links again in a future post, but do go ahead and check out the documentation for yourself as well.
In our next post, we will be covering Activation.