Successful apps turn into successful revenue generating businesses when the right business model is built into the core app development strategy from the very beginning. Since Firebase is designed to help app developers at every part of their lifecycle, from creating high-quality apps to growing and monetizing their app traffic, let’s take a peek at what monetization concepts you could be thinking about now.
Ready to start exploring AdMob?
Sign up for an AdMob account and link it to your Firebase project.
Alrighty! Thanks for joining us for part three of this blog series about the Play services Task API for Android. By now, you've seen the essentials of the API in part one, and how to select the best style of listener in part two. So, at this point, you probably have everything you need to know to make effective use of the Tasks generated by Firebase APIs. But, if you want to press into some advanced usage of Tasks, keep reading!
We know that some of the Firebase features for Android will do work for you and notify a Task upon completion. But, what if you want to create your own Tasks to perform threaded work? The Task API gives you the tools for this. If you want to work with the Task API without having to integrate Firebase into your app, you can get the library with a dependency in your build.gradle:
compile 'com.google.android.gms:play-services-tasks:9.6.1'
But, if you are integrating Firebase, you'll get this library included for free, so no need to call it out specifically in that case.
There is just one method (with two variants) you can use to kick off a new Task. You can use the static method named "call" on the Tasks utility class for this. The variants are as follows:
Task<TResult> call(Callable<TResult> callable) Task<TResult> call(Executor executor, Callable<TResult> callable)
Just like addOnSuccessListener(), you have a version of call() that executes the work on the main thread and another that submits the work to an Executor. You specify the work to perform inside the passed Callable. A Java Callable is similar to a Runnable, except it's parameterized by some result type, and that type becomes the returned object type of its call() method. This result type then becomes the type of the Task returned by call(). Here's a really simple Callable that just returns a String:
public class CarlyCallable implements Callable<String> { @Override public String call() throws Exception { return "Call me maybe"; } }
Notice that CarlyCallable is parameterized by String, which means its call() method must return a String. Now, you can create a Task out of it with a single line:
Task<String> task = Tasks.call(new CarlyCallable());
After this line executes, you can be certain that the call() method on the CarlyCallable will be invoked on the main thread, and you can add a listener to the Task to find the result (even though that result is totally predictable). More interesting Callables might actually load some data from a database or a network endpoint, and you'd want to have those blocking Callables run on an Executor using the second form of call() that accepts the Executor as the first argument.
Let's say, for the sake of example, you want to process the String result of the CarlyCallable Task after it's been generated. Imagine that we're not so much interested in the text of the resulting String itself, and more interested in a List of individual words in the String. But, we don't necessarily want to modify CarlyCallable because it's doing exactly what it's supposed to, and it could be used in other places as it’s written now. Instead, we'd rather encapsulate the logic that splits words into its own class, and use that after the CarlyCallable returns its String. We can do this with a Continuation. An implementation of the Continuation interface takes the output of one Task, does some processing on it, and returns a result object, not necessarily of the same type. Here's a Continuation that splits a string of words into an List of Strings with each word:
public class SeparateWays implements Continuation<String, List<String>> { @Override public List<String> then(Task<String> task) throws Exception { return Arrays.asList(task.getResult().split(" +")); } }
Notice that the Continuation interface being implemented here is parameterized by two types, an input type (String) and an output type (List). The input and output types are used in the signature of the lone method then() to define what it's supposed to do. Of particular note is the parameter passed to then(). It's a Task, and the String there must match the input type of the Continuation interface. This is how the Continuation gets its input - it pulls the finished result out of the completed Task.
Here's another Continuation that randomizes a List of Strings:
public class AllShookUp implements Continuation<List<String>, List<String>> { @Override public List<String> then(@NonNull Task<List<String>> task) throws Exception { // Randomize a copy of the List, not the input List itself, since it could be immutable final ArrayList<String> shookUp = new ArrayList<>(task.getResult()); Collections.shuffle(shookUp); return shookUp; } }
And another one that joins a List of Strings into a single space-separated String:
private static class ComeTogether implements Continuation<List<String>, String> { @Override public String then(@NonNull Task<List<String>> task) throws Exception { StringBuilder sb = new StringBuilder(); for (String word : task.getResult()) { if (sb.length() > 0) { sb.append(' '); } sb.append(word); } return sb.toString(); } }
Maybe you can see where I'm going with this! Let's tie them all together into a chain of operations that randomizes the word order of a String from a starting Task, and generates a new String with that result:
Task<String> playlist = Tasks.call(new CarlyCallable()) .continueWith(new SeparateWays()) .continueWith(new AllShookUp()) .continueWith(new ComeTogether()); playlist.addOnSuccessListener(new OnSuccessListener<String>() { @Override public void onSuccess(String message) { // The final String with all the words randomized is here } });
The continueWith() method on Task returns a new Task that represents the computation of the prior Task after it’s been processed by the given Continuation. So, what we’re doing here is chaining calls to continueWith() to form a pipeline of operations that culminates in a final Task that waits for each stage to complete before completing.
This chain of operations could be problematic if these they have to deal with large Strings, so let's modify it to do all the processing on other threads so we don't block up the main thread:
Executor executor = ... // you decide! Task<String> playlist = Tasks.call(executor, new CarlyCallable()) .continueWith(executor, new SeparateWays()) .continueWith(executor, new AllShookUp()) .continueWith(executor, new ComeTogether()); playlist.addOnSuccessListener(executor, new OnSuccessListener() { @Override public void onSuccess(String message) { // Do something with the output of this playlist! } });
Now, the Callable, all of the Continuations, and the final Task listener will each run on some thread determined by the Executor, freeing up the main thread to deal with UI stuff while this happens. It should be totally jank-free.
At first blush, it could seem a bit foolish to separate all these operations into all the different classes. You could just as easily write this as a few lines in a single method that do only what's required. So, keep in mind that this is a simplified example intended to highlight how Tasks can work for you. The benefit of chaining of Tasks and Continuations (even for relatively simple functions) becomes more evident when you consider the following:
Practically speaking, you're more likely to use Task continuations to perform a series of modular chain of filter, map, and reduce functions on a set of data, and keep those units of work off the main thread, if the collections can be large. But, I had fun with music theme here!
One last thing to know about Continuations. If a runtime exception is thrown during processing at any stage along the way, that exception will normally propagate all the way down to the failure listeners on the final Task in the chain. You can check for this yourself in any Continuation by asking the input Task if it completed successfully with the isSuccessful() method. Or, you can just blindly call getResult() (as is the case in the above samples), and if there was previously a failure, it will get re-thrown and automatically end up in the next Continuation. The listeners on the final Task in the chain should always check for failure, though, if failure is an option.
So, for example, if the CarlyCallable in the above chain returned null, that would cause the SeparateWays continuation to throw a NullPointerException, which would propagate to the end of the Task. And if we had an OnFailureListener registered, that would get invoked with the same exception instance.
What's the most efficient way, with the above chain, of finding out the number of words in the original string, without modifying any of the processing components? Take a moment to think about it before reading on!
The answer is probably more simple than you'd imagine. The most obvious solution is to count the number of words in the final output string, since their order only got randomized. But there is one more trick. Each call to continueWith() returns a new Task instance, but those are all invisible here because we used a chaining syntax to assemble them into the final Task. So you can intercept any of those those tasks and add another listener to it, in addition to the next Continuation:
Task<List<String>> split_task = Tasks.call(new CarlyCallable()) .continueWith(executor, new SeparateWays()); split_task = .continueWith(executor, new AllShookUp()) .continueWith(executor, new ComeTogether()); split_task.addOnCompleteListener(executor, new OnCompleteListener<List<String>>() { @Override public void onComplete(@NonNull Task<List<String>> task) { // Find the number of words just by checking the size of the List int size = task.getResult().size(); } }); playlist.addOnCompleteListener( /* as before... */ );
When a Task finishes, it will trigger both of the Continuations on it, as well as all of the added listeners. All we've done here is intercept the Task that captures the output of the SeparateWays continuation, and listen to the output of that directly, without affecting the chain of continuations. With this intercepted task, we only have to call size() on the List to get the count.
All joking aside, the Task API makes it relatively easy for you to express and execute a sequential pipeline of processing in a modular fashion, while giving you the ability to specify which Executor is used at each stage in the process. You can do this /with or without/ Firebase integrated into your app, using your own Tasks or those that come from Firebase APIs. For the next and final part to this series, we'll look at how Tasks can be used in parallel to kick off multiple units of work simultaneously.
As usual, 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 to get notified of the next post in this series.
Lastly, if you're wondering about all the songs I referenced in this post, you can find them here:
This is our third post in the Pirate Metrics with Firebase series. In the first post, we gave an overview of what Pirate Metrics are and why they’re important. In the second, we showed how you can use Firebase to improve your acquisition strategy.
Once you acquire a user, your main goal is to make them use your product. Users often install an app but never get hooked. They would have the app around for a day or two, if you’re lucky, before either forgetting about it or, worse, uninstalling it. All that effort you put into your acquisition goes down the drain.
The first few days are, hence, crucial. Through your data, you want to find a pattern to determine at what point is a user activated, and look at ways to get more users past that point. Examples can be the number of friends on a social networking application or the number of levels crossed in a video game. Devising the right “activation strategy” always involves a lot of experimentation.
To carry out these experiments, we have just the right tool for you - Firebase’s Remote Configuration. Remote Config allows you to set certain key/value pairs on the server, and use them to vary the experience inside of your application. These values when updated on the Firebase console reflect inside of your own application, allowing you to change the experience for users without releasing an update.
If you use this capability of Remote Config, and set values using the “random percentile” targeting, you essentially have an A/B test setup. You can then see the impact on your analytics, and change these values dynamically on the server itself, increasing the rollout for experiments that have proven to work.. It makes for a great solution for A/B testing.
To optimize your testing, we recommend first defining the data points you want to improve (such as an increase in users signing up on the first app open). Then, ideate on the experiments you want to run to improve these data points. These might be experiments that track the impact of different tutorials or signup methods for apps or difficulty settings for initial levels in a game that can ultimately improve your activation percentage.
Today we're excited to announce the availability of HTTP/2 on Firebase Hosting. HTTP/2 is a new version of the HTTP protocol that is already supported by 77% of browsers worldwide. It offers some exciting advantages for web developers:
Taken together, these add up to significant performance advantages and lots of opportunity to make your web applications load faster on mobile devices with slow connections.
HTTP/2 is currently enabled for all *.firebaseapp.com traffic as well as newly-provisioned custom domains. If you already have a custom domain on Firebase, see Custom Domain Migration below.
*.firebaseapp.com
To utilize HTTP/2 on Firebase Hosting, you don't have to do anything! It will automatically be served if the user's browser supports it. However, there are some best practices you should keep in mind if you want to optimize for HTTP/2:
The above guidelines aren't hard and fast rules -- as with any performance optimization, you should experiment with different combinations of optimizations to see which ones deliver the best result for your app's specific needs.
Firebase Hosting has experimental support for HTTP/2 server push using Link headers. Server push allows a server to automatically send the contents for additional resources when an initial request is made. The most common use for server push is to send down associated assets (like JavaScript or CSS files) when a page is loaded.
To configure server push on Firebase Hosting, you need to add the Link header to your firebase.json configuration like so:
firebase.json
{ "hosting": { "headers": [ { "source": "/", "headers": [{"key": "Link", "value": "</js/app.js>;rel=preload;as=script,</css/app.css>;rel=preload;as=style"}] }, { "source": "/users/*", "headers": [{"key": "Link", "value": "</js/app.js>;rel=preload;as=script,</css/app.css>;rel=preload;as=style;nopush,</css/users.css>;rel=preload;as=style"}] } ] } }
Here we are using server push to preload /js/app.js and /css/app.css on the root path, and additionally /css/users.css on any path matching /users/*. You can use the nopush directive (like on app.css in the second example) to preload the asset without HTTP/2 push for files that are likely to already be in the browser cache.
/js/app.js
/css/app.css
/css/users.css
/users/*
app.css
It's still early days for server push, and there are a few things to keep in mind:
;nopush
We're excited about HTTP/2's potential to improve that first-load experience, and we're still exploring additional ways to make server push simple, reliable, and effective for your site.
With our migration to HTTP/2 we're also moving to Server Name Indication (SNI) for our SSL certificates. SNI enables us to continue to scale our infrastructure more reliably and is supported by nearly 98% of browsers worldwide. Because this change has the possibility of affecting user traffic, we are not automatically switching over existing custom domains until December 2016.
If you have a custom domain on Firebase Hosting from before August 11, 2016, you will need to update your DNS records to take advantage of HTTP/2. You can check if you're already on SNI by running dig <your-site>.firebaseapp.com. If you see s-sni.firebaseapp.com in the result, your site is already migrated.
dig <your-site>.firebaseapp.com
s-sni.firebaseapp.com
To migrate if you're using a CNAME, update your DNS to point to s-sni.firebaseapp.com. If you're using A records, update them to:
151.101.1.195 151.101.65.195
Once you've changed over your DNS and it's had the chance to propagate, your site will be live with HTTP/2! We will be transitioning all Firebase Hosting traffic to HTTP/2 and SNI by the end of the year, so please reach out to support if you're worried about how SNI might affect your users.
Our goal with Firebase Hosting is to bring the best practices of Progressive Web App development within reach of everyone. HTTP/2 is another step along that path, and we're excited to see what you build with it!
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.