Developer Advocate at ProgressSWGuest blogger Jen Looper
The powerful combination of NativeScript, Firebase, and Angular 2 can kickstart
your app building into high gear, especially during the holidays when you find
yourself confronted with the need to speed up your app development AND meet your
family's gift-giving needs! Just in time, I am happy to present
to you (see what I did there ð) a demo of how to leverage Firebase in your Angular 2-powered NativeScript apps
using several elements of Eddy Verbruggen's famous
NativeScript-Firebase plugin.
In this tutorial, I'm going to show you how to use four popular Firebase
elements in your NativeScript app: Authentication with a login
and registration routine; Database for data storage and
real-time updates; Remote Config to make changes to an app
remotely; and Storage for saving photos. To do this, I decided
to rewrite my Giftler
app, originally written in Ionic.
Before we get started, I encourage you to read through the
documentation before starting in on your project, and make sure that a few
prerequisites are in place:
Ensure that
NativeScript is installed on your local machine and that the CLI works as
expected
Configure your preferred IDE for NativeScript and Angular development.
You're going to need TypeScript, so ensure that your transpiling process
is working. There are excellent NativeScript plugins available for
Visual Studio, Visual
Studio Code, and
Jetbrains-compatible IDEs, among others. Visual Studio Code in particular
has handy
snippets that speed up development
Log in to your Firebase account and find your console
Create a new project in the Firebase console. I named mine 'Giftler'. Also
create an iOS and Android app in the Firebase console. As part of this process
you'll download both a GoogleServices-Info.plist and google-services.json file.
Make sure you note where you place those files, and you'll need them in a
minute.
Install the dependencies
I've built Giftler as an example of an authenticated NativeScript app where
users can list the gifts that they would like to receive for the holidays,
including photos and text descriptions. For the time being, this app does the
following on iOS and Android:
allows login and logout, registration, and a 'forgot password' routine
lets users enter gift items into a list
lets users delete items from a list
lets users edit items in the list individually by adding descriptions and
photos
provides messaging from the Remote Config service in Firebase that can be
quickly changed in the backend
Now, fork the Giftler source
code, which is a complete and functional app. Once your app is cloned,
replace the app's current Firebase-oriented files that you downloaded when you
created your app:
In the /app/App_Resources/Android folder, put the google.services.json file
that you downloaded from Firebase.
Likewise, in /app/App_Resources/iOS folder, put the GoogleService-Info.plist
file also downloaded from Firebase.
These files are necessary to initialize Firebase in your app and connect it to
the relevant external services.
Now, let's take a look at the package.json at the root of this app. It contains
the plugins that you'll use in this app. I want to draw your attention to the
NativeScript-oriented plugins:
The NativeScript-Angular plugin is NativeScript's integration of Angular. The
Camera plugin makes managing the camera a bit easier. IQKeyboardManager is an
iOS-specific plugin that handles the finicky keyboard on iOS. The Theme plugin
is a great way to add default styles to your app without having to skin the app
entirely yourself. And finally, the most important plugin in this app is the
Firebase plugin.
With the dependencies in place and the plugins ready to install, you can build
your app to create your platforms folder with iOS and Android-specific code and
initialize the Firebase plugin along with the rest of the npm-based plugins.
Using the NativeScript CLI, navigate to the root of your cloned app and type tns
run ios or tns run android. This will start the plugin building routines and,
in particular, you'll see the various parts of the Firebase plugin start to
install. The install script that runs will prompt you to install several
elements to integrate to the various Firebase services. We're going to select
everything except Messaging and social authentication for the moment. A great
feature is that a firebase.nativescript.json file is installed at the root of
the app, so if you need to install a new part of the plugin later, you can edit
that file and reinstall the plugin.
At this point, if you run tns livesync ios --watch or tns livesync android
--watch to see the app running on an emulator and watching for changes, you
would see a the app running and ready to accept your new login. Before you
initialize a login, however, ensure that Firebase handles Email/Password type
logins by enabling this feature in the Firebase console in the Authentication
tab:
Let's take a look under the covers a bit to see what's happening behind the
scenes. Before you can log in to Firebase, you need to initialize the Firebase
services that you installed. In app/main.ts, there are a few interesting bits.
// this import should be first in order to load some required settings (like
globals and reflect-metadata)
import { platformNativeScriptDynamic } from "nativescript-angular/platform";
import { AppModule } from "./app.module";
import { BackendService } from "./services/backend.service";
import firebase = require("nativescript-plugin-firebase");
firebase.init({
//persist should be set to false as otherwise numbers aren't returned during
livesync
persist: false,
storageBucket: 'gs://giftler-f48c4.appspot.com',
onAuthStateChanged: (data: any) => {
console.log(JSON.stringify(data))
if (data.loggedIn) {
BackendService.token = data.user.uid;
}
else {
BackendService.token = "";
}
}
}).then(
function (instance) {
console.log("firebase.init done");
},
function (error) {
console.log("firebase.init error: " + error);
}
);
platformNativeScriptDynamic().bootstrapModule(AppModule);
First, we import firebase from the plugin, and then we call .init(). Edit the
storageBucket property to reflect the value in the Storage tab of your Firebase
console:
Now your app is customized to your own Firebase account and you should be able
to register a new user and login in the app. You can edit the user.email and
password variables in app/login/login.component.ts file to change the default
login credentials from [email protected] to your own login and password if
you like.
The iOS and Android login screens
Note: you should be able to emulate your app right away on iOS, using the
Xcode simulator. On Android, make sure that you select an Android SDK emulator image that supports Google Services, such as "Google APIs Intel x86 Atom System Image." It can be hard to get the versions lined up perfectly, so pay close attention to the Firebase dependencies and version info.
Code Structure and Authentication
Angular 2 design patterns require that you modularize your code, so we will
oblige by using the following code structure:
—login
login.component.ts
login.html
login.module.ts
login.routes.ts
—list …
—list-detail …
—models
gift.model.ts
user.model.ts
index.ts
—services
backend.service.ts
firebase.service.ts
utils.service.ts
index.ts
app.component.ts
app.css
app.module.ts
app.routes.ts
auth-guard.service.ts
main.ts
I want to draw your attention to the way Firebase authentication works with the
Angular 2 auth-guard.service. When Firebase is initialized in your app in
app/main.ts as we saw above, the onAuthStateChanged function is called:
When the app starts, check the console for the stringified data being returned
by Firebase. If this user is flagged as being loggedIn, we will simply set a
token which is the userId sent back by Firebase. We'll use the NativeScript
application settings module, which functions like localStorage, to keep this
userId available and associate it to the data that we create. This token and the
authentication tests that use it, managed in the app/services/backend.service.ts
file, are made available to the app/auth-guard.service.ts file. The auth-guard
file offers a neat way to manage logged-in and logged-out app state.
The AuthGuard class implements the CanActivate interface from the Angular Router
module.
Essentially, if the token is set during the above login routine, and the
BackendService.isLoggedIn function returns true, then the app is allowed to
navigate to the default route which is our wish list; otherwise, the user is
sent back to login:
Now that you have initialized your Firebase-powered NativeScript app, let's
learn how to populate it with data and use Firebase's amazing realtime power to
watch for the database to be updated.
Making your list, checking it twice
Starting in app/list/list.html, which is the basis of the wish list, you'll see
a textfield and a blank list. Go ahead, tell Santa what you want! The items are
sent to the database and added to your list in realtime. Let's see how this is
done.
First, note that in app/list/list.component.ts, we set up an observable to hold
the list of gifts:
public gifts$: Observable;
then, we populate that list from the database when the component is initialized:
It's in the firebaseService file that things get interesting. Note the way that
this function adds a listener and returns an rxjs observable, checking for
changes on the Gifts collection in the Firebase database:
getMyWishList(): Observable {
return new Observable((observer: any) => {
let path = 'Gifts';
let onValueEvent = (snapshot: any) => {
this.ngZone.run(() => {
let results = this.handleSnapshot(snapshot.value);
console.log(JSON.stringify(results))
observer.next(results);
});
};
firebase.addValueEventListener(onValueEvent, `/${path}`);
}).share();
}
The results of this query are handled in a handleSnapshot function below, which
filters the data by user, populating an _allItems array:
handleSnapshot(data: any) {
//empty array, then refill and filter
this._allItems = [];
if (data) {
for (let id in data) {
let result = (Object).assign({id: id}, data[id]);
if(BackendService.token === result.UID){
this._allItems.push(result);
}
}
this.publishUpdates();
}
return this._allItems;
}
And finally, publishUpdates is called, which sorts the data by date so that
newer items are shown first:
publishUpdates() {
// here, we sort must emit a *new* value (immutability!)
this._allItems.sort(function(a, b){
if(a.date < b.date) return -1;
if(a.date > b.date) return 1;
return 0;
})
this.items.next([...this._allItems]);
}
Once the data has populated your $gifts observable, you can edit and delete
elements of it and it will be handled by the listener and the front end updated
accordingly. Note that the onValueEvent function of getMyWishList method
includes the use of
ngZone which ensures that, although data updates occur asynchronously, the
UI is updated accordingly. A good overview of ngZone in NativeScript apps can be
found
here.
Remotely Configured Messages from Beyond
Another cool piece of Firebase's service includes "Remote Config", a way to
provide app updates from the Firebase backend. You can use Remote Config to
toggle features on and off in your app, make UI changes, or send messages from
Santa, which is what we're going to do!
In app/list/list.html, you'll find a message box:
<Label class="gold card" textWrap="true" [text]="message$ | async"></Label>
The message$ observable is built in much the same way as the data list; changes
are picked up in this case each time the app is freshly initialized:
And the magic occurs in the service layer (app/services/firebase.service.ts ):
getMyMessage(): Observable{
return new Observable((observer:any) => {
firebase.getRemoteConfig({
developerMode: false,
cacheExpirationSeconds: 300,
properties: [{
key: "message",
default: "Happy Holidays!"
}]
}).then(
function (result) {
console.log("Fetched at " + result.lastFetch + (result.throttled ? "
(throttled)" : ""));
for (let entry in result.properties)
{
observer.next(result.properties[entry]);
}
}
);
}).share();
}
Publish new messages as often as you like!
Note: tinkering repeatedly with Remote Config may cause throttling of your
Firebase instance, so develop with care
Take a picture!
One of the more interesting parts of this project, I think, is the ability to
take a picture of your present of choice and store it in Firebase Storage. I
leveraged the Camera plugin, as mentioned above, which makes managing the
hardware a little easier. To start, ensure that your app has access to the
device camera by getting permissions set in the ngOnInit() method in
app/list-detail/list-detail.component.ts:
ngOnInit() {
camera.requestPermissions();
...
}
A chain of events begins when the user clicks the 'Photo' button in the detail
screen. First,
takePhoto() {
let options = {
width: 300,
height: 300,
keepAspectRatio: true,
saveToGallery: true
};
camera.takePicture(options)
.then(imageAsset => {
imageSource.fromAsset(imageAsset).then(res => {
this.image = res;
//save the source image to a file, then send that file path to
firebase
this.saveToFile(this.image);
})
}).catch(function (err) {
console.log("Error -> " + err.message);
});
}
The camera takes a picture, and then that photo is stored as an imageAsset and
displayed on the screen. The image is then named with a date stamp and saved to
a file locally. That path is reserved for future use.
Once the 'Save' button is pressed, this image, via its local path, is sent to
Firebase and saved in the storage module. Its full path in Firebase is returned
to the app and stored in the /Gifts database collection:
editGift(id: string){
if(this.image){
//upload the file, then save all
this.firebaseService.uploadFile(this.imagePath).then((uploadedFile: any) =>
{
this.uploadedImageName = uploadedFile.name;
//get downloadURL and store it as a full path;
this.firebaseService.getDownloadUrl(this.uploadedImageName).then((downloadUrl:
string) => {
this.firebaseService.editGift(id,this.description,downloadUrl).then((result:any)
=> {
alert(result)
}, (error: any) => {
alert(error);
});
})
}, (error: any) => {
alert('File upload error: ' + error);
});
}
else {
//just edit the description
this.firebaseService.editDescription(id,this.description).then((result:any)
=> {
alert(result)
}, (error: any) => {
alert(error);
});
}
}
This chain of events seems complicated, but it boils down to a few lines in the
Firebase service file:
The end result is a nice way to capture both photos and descriptions of the
gifts for your wish list. No more excuses that Santa didn't know exactly WHICH
Kylie Eyeliner to buy. By combining the power of NativeScript and Angular, you
can create a native iOS and Android app in a matter of minutes. By adding
Firebase you have a powerful way of storing your app's users, images and data,
and a way of updating that data in real-time across devices. Cool, huh? It looks
like this:
We are well on our way to create a solid wishlist management app! It remains to
figure out the best way to inform Santa of our wishes - a Mailgun email
integration or using push notifications would be the obvious next route. In the
meantime, best wishes for a wonderful holiday season, and I hope you have a
great time creating awesome NativeScript apps using Firebase!
Want to learn more about NativeScript? Visit http://www.nativescript.org. If you need
help, join the NativeScript Slack channel here.
The Santa
Tracker app for Android is a Google holiday tradition. Every year, millions
of people around the world use the app to play games with elves and reindeer
and, of course, track Santa, as he flies around the world on December 24th.
While the app is live for a few months each year, about 90% of our usage occurs
in the last two weeks of December. In order to turn around improvements to
Santa Tracker quickly over this time, it's critical that we can monitor and
adjust the Santa Tracker app remotely. This year, we decided to go all-in with
Firebase as our monitoring solution. In this blog post, I'll talk about how we
use a combination Analytics, Crash Reporting, and Remote Config to
maintain a high level of quality, without ever having to republish the app.
Firebase Analytics
As users navigate through the app we use Firebase Analytics events to record
their behavior. Most of the mini-games in the app live in their own Activity
classes, so we can use Firebase Analytics' automatic screen tracking feature to
record these events without writing any code.
For events within games we use custom
events to record important user actions. For example after the user finishes
playing the "Penguin Swim" game, we record the event
swimming_game_end with custom parameters score and
num_stars. In the first week of December we noticed that 85% of
users were getting zero stars when playing the Penguin Swim game. Clearly, the
game is too hard, we were hoping that only 60-70% of users would get a score
this low! We were able to correct this using Remote Config, which I'll talk
about later.
The other feature of Analytics that we put to use is user
properties. At the start of each Santa Tracker session, we use user
properties to record some information about the user's device. These properties
are then attached to every analytics event. Since Santa Tracker is used all over
the world, we get a lot of diversity in the devices people use. These user
properties help us to make sense of our analytics data. Some examples are:
API_LEVEL
The API level of the user's device, like 23 for Marshmallow or 21 for
Lollipop.
DEVICE_BRAND
The brand of the user's device, such as Samsung for the Galaxy S7 or
Google for the Pixel XL.
DEVICE_BOARD
The processor or specific SoC name for the user's device.
The combination of our custom events and user properties with Firebase
Analytics' automatically tracked events enables us to get a good understanding
of what our users are doing in the app by looking at the Firebase console.
Firebase Crash Reporting
Despite our best efforts, the Santa Tracker app is not perfect. With millions
of users on hundreds of device types in dozens of countries we are constantly
discovering new bugs in the wild. Firebase Crash Reporting lets us see all of
the fatal errors in our app within a minute of their occurrence. Since Firebase
Analytics events show up in Firebase Crash Reporting logs we can see the
progression of events before the crash which was very helpful in diagnosing some
issues.
For example there's an OutOfMemoryError crash which seems to happen
during the "Penguin Swim" game on some low-RAM devices. We did not see this
error during our testing, but the Firebase Analytics data in Crash Reporting
tells us that this occurs when playing the game repeatedly.
This integration is invaluable in helping us to reproduce issues that our normal
QA setup does not find. We can get the exact device model and then use the
analytics log to recreate the crash conditions.
Firebase Remote Config
Once we have analyzed the data from Analytics and Crash Reporting, we need to
make changes in the app to improve the user experience. Due to the short active
life span of this app there's no time to go through the full development
lifecycle of the app to publish changes, and we don't get a second chance at
Santa's big day!
Santa Tracker uses Firebase Remote Config to gate access to various features,
and to provide remote fine-tuning for experiences in the mini game. For
example, in the "Penguin Swim" game, there are two key variables we store in
Remote Config:
SwimmingObstacleDensity
Control the density of ice cubes and other obstacles in the game, a lower
density makes the game easier.
DisableSwimmingGame
A kill-switch to completely hide the game from the app.
As mentioned earlier, users were having a hard time getting a score higher than
zero stars in the game. In order to make the game more fun, we changed
SwimmingObstacleDensity from 1.5 to 1.1, which made it much easier
for users to dodge obstacles. By making the game easier in this way, the
percentage of users getting 0 stars went down from about 85% to 70%. This
change took place instantly over the air, with no need to publish a new version
of the app!
Right now the OutOfMemoryError in the swimming game happens for <1%
of users. But if this issue became rampant, we could use the
DisableSwimmingGame flag to immediately hide the game from affected
users whilst we resolve the issue. By taking advantage of the fact that
Analytics user properties can be referenced in Remote Config, we can even
disable the game only for certain device types! For example, let's say the
Penguin Swim stopped working on all KitKat devices (API level 19).
First, we add a condition based on user properties:
Next, we disable the game only for users who match the condition:
Now the game will only appear for users who will have a stable experience, which
will lead to fewer crashes for our users and more positive app ratings for us.
Final Thoughts
Adding deep Firebase integration to Santa Tracker gives us the ability to
monitor and fine-tune the app over time without releasing app updates. As
developers, it's invaluable to have a clear picture of what our users are really
doing and how we can improve the app. Throughout December we knew we could rely
on Firebase to give Santa Tracker users a magical holiday experience.
If you've been working with Firebase on Android, you may have noticed that you
don't normally have to write any lines of code to initialize a feature. You
just grab the singleton object for that feature, and start using it right away.
And, in the case of Firebase Crash Reporting, you don't even have to write any
code at all for it to start capturing crashes! This question pops
up from time to time, and I talked about it a bit at Google
I/O 2016, but I'd also like to break it down in detail here.
The problem
Many SDKs need an Android Context
to be able to do their work. This Context is the hook into the Android runtime
that lets the SDK access app resources and assets, use system services, and
register BroadcastReceivers. Many SDKs ask you to pass a Context into a static
init method once, so they can hold and use that reference as long as the app
process is alive. In order to get that Context at the time the app starts up,
it's common for the developers of the SDK to ask you to pass that in a custom Application
subclass like this:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SomeSdk.init(this); // init some SDK, MyApplication is the Context
}
}
And if you hadn't already registered a custom subclass in your app, you'd also
have to add that to your manifest in the application
tag's android:name attribute:
All this is fine, but Firebase SDKs make this a lot easier for its users!
The solution
There is a little trick that the Firebase SDKs for Android use to install a hook
early in the process of an application launch cycle. It introduces a ContentProvider
to implement both the timing and Context needed to initialize an SDK, but
without requiring the app developer to write any code. A ContentProvider is a
convenient choice for two reasons:
They are created and initialized (on the main thread) before all
other components, such as Activities, Services, and BroadcastReceivers, after
the app process is started.
They participate in manifest merging at build time, if they are declared in
the manifest of an Android library project. As a result, they are automatically
added to the app's manifest.
Let's investigate those two properties.
ContentProvider initializes early
When an Android app process is first started, there is well-defined order of
operations:
Each ContentProvider declared in the manifest is created, in priority
order.
The Application class (or custom subclass) is created.
If another component that was invoked via some Intent, that is
created.
When a ContentProvider is created, Android will call its onCreate method. This
is where the Firebase SDK can get a hold of a Context, which it does by calling
the getContext
method. This Context is safe to hold on to indefinitely.
Manifest
merge is a process that happens at build time when the Android build tools
need to figure out the contents of the final manifest that defines your app. In
your app's AndroidManifest.xml file, you declare all your application
components, permissions, hardware requirements, and so on. But the final
manifest that gets built into the APK contains all of those elements from
all of the Android library projects that your app depends on.
It turns out that ContentProviders are merged into the final manifest as well.
As a result, any Android library project can simply declare a ContentProvider in
its own manifest, and that entry will end up in the app's final manifest. So,
when you declare a dependency on Firebase Crash Reporting, the ContentProvider
from its manifest is merged in your own app's manifest. This ensures that its
onCreate is executed, without you having to write any code.
FirebaseInitProvider (surprise!) initializes your app
All apps using Firebase in some way will have a dependency on the
firebase-common library. This library exposes FirebaseInitProvider, whose
responsibility is to call FirebaseApp.initializeApp
in order to initialize the default FirebaseApp instance using the configurations
from the project's google-services.json file. (Those configurations are
injected into the build as Android resources by the Google
Services plugin.) However, If you're referencing multiple Firebase projects
in one app, you'll have to write code to initialize other FirebaseApp instances,
as discussed in an earlier
blog post.
Some drawbacks with ContentProvider init
If you choose to use a ContentProvider to initialize your app or library,
there's a couple things you need to keep in mind.
First, there can be only one ContentProvider on an Android device with a given
"authority"
string. So, if your library is used in more than one app on a device, you have
to make sure that they get added with two different authority strings, or the
second app will be rejected for installation. That string is defined for the
ContentProvider in the manifest XML, which means it's effectively hard-coded.
But there is a trick you can use with the Android build tools to make sure that
each app build declares a different authority.
There is a feature of Android Gradle builds call manifest
placeholders that lets you declare and insert a placeholder value that get
inserted into manifest strings. The app's unique application ID is
automatically available as a placeholder, so you can declare your
ContentProvider like this:
The other thing to know about about ContentProviders is that they are only run
in the main process of an app. For a vast majority of apps, this isn't a
problem, as there is only one process by default. But the moment you declare
that one of the Android components in your app must run in another process, that
process won't create any ContentProviders, which means your ContentProvider
onCreate will never get invoked. In this case, the app will have to either
avoid calling anything that requires the initialization, or safely initialize
another way. Note that this behavior is different than a custom Application
subclass, which does get invoked in every process for that app.
But why misuse ContentProvider like this?
Yes, it's true, this particular application of ContentProvider seems really
weird, since it's not actually providing any content. And you
have to provide implementations of all the other ContentProvider required
methods by returning null. But, it turns out that this is the most reliable way
to automatically initialize without requiring extra code. I think the
convenience for developers using Firebase more than makes up for this
strangeness of this use of a ContentProvider. Firebase is all about being easy
to use, and there's nothing easier than no code at all!
Firebase provides a bunch of features to use together in your app, provided by a
project that you create at the Firebase console. Normally, it's sufficient to
have all your app's resources provided by a single project, but there are times
when you want a single app to be able to access data from multiple projects.
For example, you may need to access data from two different databases, and be
able to authenticate users to access each one. I'll show you how that's done in
this post.
First, a little terminology:
Legacy Firebase.com Project
A project created on the legacy console, associated with a Firebase
Database that has not been upgraded to the new console.
A project created on the new Firebase console. Every Firebase project is
also a Google API project underneath.
App
A client for a specific platform. Each project can have multiple apps
associated with it..
Upgraded legacy Firebase.com project along with existing Google API
project
One particular scenario occurs for developers who want to upgrade their
existing legacy Firebase.com database to a new Firebase project, while also
being able to use services from another Google API project. The upgraded legacy
project becomes a new Firebase project, and that needs to be used in tandem with
the Google API project that provides Google Sign-In authentication for existing
users.
The challenge here is that for the Google Sign-In component to work on Android,
it requires a SHA-1 (the fingerprint of the key used to sign the APK) and
package name (e.g. com.foo.bar) to be registered for the app. This combination
allows Google Sign-In to know which Google API project is being used by a
particular app. A given pair of SHA1 and Package Name is globally unique within
Google (and Firebase projects), so if you try to add the same pair SHA-1 and
package name to an upgraded Firebase project, you get an error that the OAuth2
client already exists (in the Google API project):
Warning: If you see this, don't delete your existing client ID
for apps in production! This will break your app for your existing users. The
right choice is to create a new app with the same your package name in the
Firebase console for the upgraded project, but not to include a SHA1.
Now implement Google Sign
In with Firebase Auth as normal. At one point you will have to configure
your Google Sign Options object:
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build();
The default_web_client_id string here is used to set the
audience field of the ID token. The value comes from the
google-services.json file, which is from the Firebase project rather than the
Google project. You'll need to replace it with a client ID from the Google
project. You can use any Web client ID, or create a new one:
Next, back in the Firebase project, whitelist the client ID you just set for the
GoogleSignInOptions in the Auth > Sign In Providers > Google
section of the Firebase console.
Be sure to re-download your google-services.json and add it to your
Android app. At this point, your Firebase project will accept Google ID tokens
generated by your Google project - so your Android app will happily sign in to
Google using the Google project, then authenticate with your Firebase project
using the Google ID token following the normal
approach. You'll be able to make authenticated calls to Google APIs
associated with the Google API project, and authenticated calls to Firebase APIs
using the Firebase project.
Accessing the Databases from two different Firebase projects
In the previous situation, we had a single Firebase project which needed to also
access a Google project. That works because the APIs are separate. However,
sometimes you need to access different projects using the same APIs - for
example, accessing multiple database instances.
For Android apps using Firebase, there is a central FirebaseApp
object that manages the configuration for all the Firebase APIs. This is
initialized automatically by a content provider when your app is launched, and
you typically never need to interact with it. However, when you want to access
multiple projects from a single app, you'll need a distinct FirebaseApp to
reference each one individually. It's up to you to initialize the instances
other than the default that Firebase creates for you.
For example, to connect to the default Firebase Database instance, we implicitly
use the default Firebase app:
To connect to another Firebase Realtime Database from another project, you first
need to initialize a FirebaseApp instance for that other Firebase project, and
give it an identifier - in this case "secondary":
FirebaseOptions options = new FirebaseOptions.Builder()
.setApplicationId("1:530266078999:android:481c4ecf3253701e") // Required for Analytics.
.setApiKey("AIzaSyBRxOyIj5dJkKgAVPXRLYFkdZwh2Xxq51k") // Required for Auth.
.setDatabaseUrl("https://project-1765055333176374514.firebaseio.com/") // Required for RTDB.
.build();
FirebaseApp.initializeApp(this /* Context */, options, "secondary");
Then, you can access the database using the same client APIs, but this time specifying which project you want to access by passing the relevant FirebaseApp to FirebaseDatabase.getInstance():
// Retrieve my other app.
FirebaseApp app = FirebaseApp.getInstance("secondary");
// Get the database for the other app.
FirebaseDatabase secondaryDatabase = FirebaseDatabase.getInstance(app);
Authenticating to two different Firebase Databases
We can combine the two techniques above to allow sharing authentication data
between Firebase project whenever you have an external ID to join on.
For example, if our app allows sign in with Google Sign-In, and we have
configured our database rules in our default and secondary projects to require
authentication, we can use the same Google credential to log in to both systems.
First, we set up the app for Google Sign-In on the default project as normal.
Then, we get the underlying client ID from the default project. A client ID is
just an identifier for a given app client (web, Android, iOS) that is usually
contained within the client itself. A project can have several client IDs, but
the one we need to whitelist is the one specified in the requestIdToken call to
the GoogleSignInOptions
builder:
With one sign in from the user, they are authenticated against both projects.
Sharing UIDs Between Projects
One challenge here is that the Firebase user IDs on each project will be
different. For example, using the same Google credential I get these two UIDs:
If the app doesn't offer account
linking, we can use the Google (or Facebook, Twitter, etc.) user ID for
things like database structures and security rules. However, if we need the same
user ID in each project, or we're using email/password or anonymous auth, the
situation is slightly trickier.
Luckily, it can be resolved using custom auth facilities, along with some server
side code, since custom auth tokens get to specify their own UID!
This time, we don't whitelist anything on the secondary project, but we do download
the service account for both it and our default projects. In our Android
client, we first sign in and grab the Firebase ID token from the FirebaseAuth
client:
Note: We can use any sign in provider we want here! We're just using the
custom token to link our user IDs across projects.
firebaseAuth.getCurrentUser().getToken(false /* forceRefresh */)
.addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
String token = task.getResult().getToken(); // Send this to the server.
}
});
We send that to our server, where we use it create a Firebase custom token. Just
as on Android, we need to initialise each of our apps, though we use service
accounts as we're server side (here we're using the Java server SDK, but you
could use NodeJS similarly).
FirebaseOptions options = new FirebaseOptions.Builder()
.setServiceAccount(new FileInputStream("default-service-account.json"))
.build();
FirebaseApp.initializeApp(options);
FirebaseOptions secondaryOptions = new FirebaseOptions.Builder()
.setServiceAccount(new FileInputStream("secondary-service-account.json"))
.build();
FirebaseApp.initializeApp(secondaryOptions, "secondary");
The primary app is used to verify the token coming from the client, and the
secondary to create the custom auth token with the appropriate UID set:
// Verify the ID token using the default app.
FirebaseAuth.getInstance().verifyIdToken(idToken)
.addOnSuccessListener(new OnSuccessListener() {
@Override
public void onSuccess(FirebaseToken decodedToken) {
String uid = decodedToken.getUid();
System.out.println("User " + uid + " verified");
FirebaseApp app = FirebaseApp.getInstance("secondary");
String customToken = FirebaseAuth.getInstance(app).createCustomToken(uid);
// TODO: Send the token back to the client!
}
});
Back in the Android app, we take the custom token from the server and use it to
authenticate to the secondary project.
Hopefully, this helps offer some options for dealing with multiple Firebase
projects from a single app. If you're wondering whether the same thing works on
iOS and the web - it absolutely does. You just need to use the equivalent to
Android's FirebaseApp to create a reference to the secondary project.
With JavaScript, you use firebase.app:
var config = {
apiKey: "",
authDomain: ".firebaseapp.com",
databaseURL: "https://.firebaseio.com",
storageBucket: ".appspot.com",
messagingSenderId: "",
};
var secondary = firebase.initializeApp(otherAppConfig, "secondary");
var secondaryDatabase = secondary.database();
Over the course of this series, I've introduced the concept
of Pirate Metrics, followed by individual posts discussing how to track (and
improve) acquisition,
activation
and retention
with Firebase and its' suite of products.
Every product owner dreams of seeing the work they create go viral. When your
users love what you built so much that they want everyone around them to use it
as well, it validates all the effort and hard work that went into it.
But here's the thing: while your users are typically more than happy to refer
your application to their friends, family and colleagues, they are unlikely to
be interested in putting in a lot of effort. The simple, easy way is to ensure
you make it easy to simply share the URL for your application. However, you want
to easily track how your referrals are doing, and you also want to smoothen the
onboarding process for the new, incoming user.
The first product I'd like to talk about in this post is Dynamic Links. These
links, which can be generated either on the Firebase console or
programmatically, offer the benefit of redirecting users appropriately based on
where they're opening them. That is, users on Android can be automatically sent
to the Play Store while users on iOS can be sent to the App Store. If the user
already has the app, they can be deeplinked to specific content inside it.
You can take Dynamic Links a few steps further as well by taking advantage of
the fact that the data associated with each links survives the app installation
process. This means that if the link was meant to deeplink to specific content
(such as a product in an E-commerce service), you can take the user straight to
the appropriate page once the new user completes the installation.
You can also consider using Dynamic Links to personalize the onboarding process
for new users. For example, if you provide a referral bonus to users which
relies on sharing of codes, you could ensure the link has this code already
added as a parameter and add it automatically for the user right after install.
This offers the opportunity to earn a lot of goodwill.
Links are only one part of the solution we have to offer - if you also use Firebase Invites, you can
provide users with a simple options of choosing which contacts they would like
to share your app with instead of relying upon third party apps. This list of
contacts is also sorted to highlight those people the user frequently
communicates with.
Firebase Invites referrals are sent over SMS and E-mails, and give you the
benefits of Dynamic Links mentioned before. You can customize the invitation
content, including the ability to specify templates with rich HTML content for
E-mail. And you don't need the user to be signed in to your service or know
their E-mail address either.
Your users are your best advocates, and we highly recommend minimizing the
friction that might prevent them from sharing your application with the other
people in their lives. Regardless of what your product is, you are likely to
benefit from gently nudging - especially your more active ones.
Firebase Crash Reporting
has enjoyed rapid adoption since its beta launch at Google I/O 2016. So far, we
helped identify hundreds of millions of errors to help developers provide the
best possible experience for users. Firebase Crash Reporting is now fully
released, with many new features and enhancements to help you better diagnose
and respond to crashes that affect the users of your iOS and Android mobile
applications. Read on to discover what's new!
Issue Resolution
One of the most hotly requested features is the ability to mark an error cluster
as "closed" in the dashboard, in order to indicate that the issue should be
fixed, and that the next release should no longer generate that particular kind
of crash. In the event of a regression in a future version, the crash cluster
will be automatically reopened for context.
Improved Reporting Latency
The time it takes for a crash to be reported until the moment it appears in your
console has been drastically decreased from about twenty minutes to less than a
minute. We expect this improvement, in addition to email alerts, will improve
your ability to diagnose errors as they happen.
Email Alerts
Anyone who has access to your Firebase project can arrange to receive an email
alert if we see brand new clusters of errors, or errors that have regressed
after being marked as closed. You can use this to quickly triage and respond to
errors, in order to minimize the impact of a defect on your users.
Analytics events in Crash Logs
Firebase Analytics
events are now added to your crash logs, which gives you a more complete view of
the state of your app leading up to crash. This added context will also help you
observe how crashes may be impacting your revenue and critical conversion
events.
Mobile-Friendly Console
The Crash Reporting console has been improved for use on mobile devices. Its
new responsive design makes it easy to check on the health of your apps when
you're away from your desktop computer.
Android SDK Compatibility
The first release of the Android SDK had a limitation that prevented it from
working well with some apps that declare an Application class. This limitation
has been resolved, and Firebase Crash Reporting should work well with any
Android app.
Updated Support for Swift on iOS
The service has been updated to show symbols from apps written in Swift 2 and 3.
We want your feedback!
If you decide to give Firebase Crash Reporting a try, please let us know how it
went for you. For any questions about Crash Reporting or any other Firebase
feature, please use the firebase-talk
forum, or if it's a programming question, you can use the firebase tag on
Stack Overflow.
Update: Both DebugView and StreamView are available to all Firebase developers, so you can get access to your data a whole lot faster! Happy Analytics viewing!
This is probably one of the most common questions we get around Firebase Analytics, and
we thought it was worth taking some time to delve into this topic a little
deeper. So buckle in, kids! We've got some learning ahead of us...
To understand latency with Firebase Analytics, there are two potential delays
you need to be aware of:
The time it takes for analytics data on the client to be sent over to the
Firebase Analytics servers.
The time it takes for Firebase Analytics data to be displayed to you in the
Firebase console once it's been received on the server.
Let's go over these one at a time.
Client-to-server latency
Firebase Analytics doesn't constantly stream down data from the client device.
That would be a serious battery drain, and we want to be respectful of your
users' battery life. Instead, analytics data is batched up and sent down when
the client library sees that there's any local data that's an hour old.
On iOS devices (and Android devices without Google Play Services), this one hour
timer is per app. But on Android devices with Play Services, this one hour timer
is across all apps using Firebase Analytics.
In addition, Firebase Analytics will send down all of its data from the client
if your user triggers a conversion event (like making an in-app purchase). And
on iOS devices, Firebase Analytics will also send down its data whenever your
app goes into the background.
"Hang on there -- once an hour? What about my users who churn out sooner
than that?"
Not to worry. On most Android devices, it's Google Play Services that manages
sending down this data. Which means that even if your user deletes your app
after an hour, that data will still get sent down because Google Play Services
still has it.
On iOS devices, Firebase will also send down data when your app moves into the
background. So if a user tries your app for 20 minutes and then uninstalls it,
that session data will still be received, because your app will have sent down
the data the moment the user moved your app to the background. The one corner
case where analytics data would actually get lost on iOS would be if your app
crashed and then your user immediately uninstalled it.
Server-to-console latency
Now, moving on to the second case, there's also the frequency at which Firebase
Analytics grabs the latest batch of data it's received from your client and uses
that data to update the reports and graphs you see in the Firebase Console. This
process typically runs every few hours. So that's the kind of delay you should
expect after your client data is sent down to the server.
If you're looking for your most recent data in these reports, keep in mind that
the default "Last 30 days" report doesn't include any data from the current day
-- this is because the data for the current day is incomplete, and it would be
misleading (not to mention a bummer) to see every graph end with a giant
downturn in usage. So if you want to see the current day's data, you'll want to
select "Today" from the drop down menu in the Firebase Console.
On the other hand, if you've set up your app to export all of its Firebase
analytics data to BigQuery, this data is available for you to look at
right away. There are no batch reports that need to be run, so you can
immediately view all of the day's data by looking at the app_events_intraday
table that's automatically created for you in BigQuery. For more about this
feature, be sure to check out our earlier
blog post.
But outside of BigQuery, it generally takes a few hours for you to see any data
you've recorded in Firebase Analytics.
"Anything that can be done to speed up this process?"
As you may have heard at our Firebase
Dev Summit, we've working on two enhancements to Firebase Analytics --
DebugView and StreamView -- which will give you more up-to-date insight into
your analytics data during both development and production.
Neither of these are yet available to the general public, but as your reward for
making it this far into the blog post, here's a link to
sign up for the DebugView closed beta. See? Reading has its advantages!
"So, what if I want a some kind of realtime dashboard? What should I
do?"
There are many developers out there who want to be notified as soon as they see
something unusual in their stats -- whether that's a sudden drop in in-app
purchases, people failing to make it through the tutorial, or what-have-you.
And while you can't quite accomplish this with the free Firebase Analytics
reports that you see in the console, you could accomplish this sort of thing by
combining BigQuery with another tool such as Google Data Studio, a third-party
visualization tool like Tableau, or even writing your own Google Apps Script
monitoring script. All of which allow you to run some pretty sophisticated
custom reports, but frankly, that's a whole other blog post.
Do keep in mind, however, that you're still subject to BigQuery usage charges
when you query your data though these tools if you go beyond the 1TB/month free
tier, so be mindful of how much (and how frequently) you decide to process your
data.
Hopefully, this gives you a better understanding of how long it takes for you to
see analytics data and what you can expect when you're developing your app. Now
go forth and start recording those events!
If your app is using Firebase Realtime
Database, you've probably gotten a lot of mileage out of its ability to
notify your app quickly as changes are made to the database. Your listeners are
triggering and receiving new data, users are delighted, and all is well with the
world.
However, sometimes listeners may not behave in exactly the way you'd
expect when combined with security and validation rules.
There are some important nuances to the way Firebase Realtime Database works,
and how those nuances affect the way your listeners are triggered. Let's go
over some of those situations, so you can expect the unexpected!
Security Rules with Value Event Listeners
Are you using security and
validation rules to protect access to your data? If not, please take a good
hard look at that! But if you are using these rules, you can run into
some behavior that may seem confusing at first, but is actually predictable,
once you understand how the Firebase Realtime Database client libraries work.
All the code samples here will be in Java, because Android is my main thing.
But the principles apply to each of the supported platforms, including iOS, web,
and JavaScript on the server side.
Imagine you have the following database rules set up:
{
"rules": {
".read": true,
".write": false
}
}
So, basically, everything is readable and nothing is writable. Your security
rules are likely going to be much more specialized, but the point is that some
writes will not be allowed at certain locations or under certain circumstances.
I'm keeping it simple here, in case you want to experiment with the code samples
here in a new project.
Now imagine you have the following tiny bit of data in your database:
ROOT
- data
- value: 99
You'd expect that a ValueEventListener
on the /data node would give you a snapshot containing a map of the key "value"
to the number 99. So, if you executed this code, you'd get a single log
statement showing these details:
private class MyValueEventListener implements ValueEventListener {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
Log.i("********** change", dataSnapshot.getKey() + ": " + dataSnapshot.getValue());
}
@Override
public void onCancelled(DatabaseError databaseError) {
// If we're not expecting an error, report it to your Firebase console
FirebaseCrash.report(databaseError.toException());
}
}
Pretty straightforward. But imagine you then attempt to change the value from
99 to 100:
HashMap map = new HashMap<>();
map.put("value", 100);
dataRef.setValue(map);
Since our security rules prohibit this, we expect to fail. And it does. But
one other thing happens that may not be expected. If MyValueEventListener is
still registered at the time setValue() is called, it will also be triggered
with the new value of 100. Not only that, but the listener will be
triggered again with the original value of 99. Your app log might look
something like this:
So we see here that the listener got the original value of 99, then the updated
value of 100, then an error, then back to the original 99.
Now, you might be thinking, "The security rules should have prevented that
change to 100! What gives!" This is a completely understandable perspective.
However, it's time to update your expectations with some knowledge about what's
really going on here!
The client SDK has no knowledge of the security rules for your project. They
live and are enforced on the Firebase server side. However, when the SDK
handles the call to setValue(), it goes ahead and assumes that the update will
actually work on the server. This is the usual case for code that's been
written for a database with a particular set of rules —
the intent is typically never to violate any rules. With this assumption in
play, the SDK goes ahead and acts early, as if the write to the
database location has actually succeeded. The result of this is the triggering
of all listeners currently added to the changed location within the same app
process.
OK, so, you might be wondering: if a write can fail, why does the client SDK act
early like this? The reasoning is that these immediate callbacks can help your
app feel snappy in the face of a poor network connection, and also allows your
app to be usable when completely offline. For example, if a user wants to make
a change to their profile, why not let them see that change immediately, rather
than having to wait for a full round trip to the server? After all, if your
code intends to honor the security rules, there should be no problem, right?
In the case where your code does violate a security rule like this, the server
notifies the app that the update actually failed at that location. The logical
thing to do, at this point, is trigger all listeners at that location with the
original data, so the UI of your app can regain consistency with known values
from the server.
Given all this context on how security rules works, let's look at another
scenario.
Security Rules with Child Event Listeners
Child
event listeners are different from the value event listeners described
above. A ValueEventListener
as shown above gives you the entire contents of a particular location, every
time any part of it changes, whereas a ChildEventListener
gives you callbacks for individual child nodes under a location whenever one of
those children is added, changed, moved, or removed.
For this example, let's use the same security rules as before, with everything
readable and nothing writable:
{
"rules": {
".read": true,
".write": false
}
}
Now, let's say you have a node in your database called /messages, where you want
users to be able to push new message content to be shared with others:
In this code, we have a ChildEventListener added on /messages, then we're trying
to add a new child object into a location determined by some generated push id.
Of course, we expect this to fail because of the security rules. But, let's
look at the log to see what actually happens if we execute this code:
I/**********: childAdded DataSnapshot { key = -KTfacNOAJt2fCUVtwtj, value = {key=value} }
W/RepoOperation: setValue at /messages/-KTfacNOAJt2fCUVtwtj failed: DatabaseError: Permission denied
I/**********: childRemoved DataSnapshot { key = -KTfacNOAJt2fCUVtwtj, value = {key=value} }
We see that the client library immediately triggers the onChildAdded method with
the new child object under /messages, then logs an error, then triggers the
onChildRemoved callback with the same object.
If you read through and understood the prior example, this one should be a
little less surprising. The Firebase client SDK is again acting early
in response to the call to setValue() and assuming that the write will success.
Then, after the write fails because of the security rules, it attempts to "undo"
the add that failed. This ensures that the app's UI can remain up-to-date with
the correct child values, assuming that it has implemented onChildRemoved
correctly.
The behavior of the Firebase client library in the face of violated security
rules should be more clear now, but you might still be wondering how you can
detect if a violation occurred. It may not be adequate for your app to simply
reverse the effect of the write. In fact, you may even want to know if and when
that actually happens, as it could be considered a programming error. This
brings me to the next point.
Detecting Write Errors
In the examples above, it can be very difficult to tell if your call to
setValue() failed at the server just by looking at the listener callbacks. If
you want to detect failure, you'll need a bit of extra code to respond to that
event. There are two ways to do this. First, there is CompletionListener
that you can pass to an overload of setValue
that gets notified of errors. Alternatively, you can also use the Play Services Task
API by using the Task
object returned by setValue. I'll prefer a Task here, because it has built-in
protections against Activity leaks (note the first argument to addOnCompleteListener
is an Activity instance):
Task task = messageRef.setValue(map);
task.addOnCompleteListener(MainActivity.this, new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
Log.i("**********", "setValue complete");
if (!task.isSuccessful()) {
Log.i("**********", "BUT IT FAILED", task.getException());
FirebaseCrash.log("Error writing to " + ref.toString());
FirebaseCrash.report(task.getException());
}
}
});
When the write of the value completes, with either success or error, the OnCompleteListener
registered to the Task will be called. If it failed, I can check the Task to
see if it was successful and deal with it as needed. In the above code, I'm
choosing to report the error to Firebase Crash Reporting,
which can help me determine if and where I made a mistake in my code or security
rules. It's probably a good idea to always report your write failures like
this, unless you fully expect that a write could legitimately fail, under normal
circumstances, to a security rule.
When there is an update to a location that also has active listeners in the same
process, the flow of data through the process goes like this:
Immediately call all relevant listeners with the new value
Send the update to the Firebase server side
Check security rules for validity
If a security rule was violated, notify the client SDK
Roll back the change in the app by calling relevant listeners again to back
to the original state
Using this knowledge, it's possible you may have reset your expectations to
expect the unexpected for your listeners! Were your expectations changed? Let
me know in the comments below! And, if you have any programming questions about
Firebase Realtime Database, you can ask us on Stack Overflow with the firebase-database
tag. For more general questions, you can ask on Quora or use the firebase-talk
Google Group.
If you like, follow me on Twitter as CodingDoug, and don't forget to check
out our YouTube channel for Firebase
tutorials and other shows.