An Android client for Google's Firebase project. This app was built for GDG Riga's Firebase Workshop event which took place on 27th of August 2016 in Riga, Latvia. Follow this guide to get started with Firebase development on Android.
- Device with Android 2.3+ and Google Play Services 9.4.0+
- Android Studio 1.5+
- Google Play Services SDK
A sprint board (something like Trello) that utilizes most of the Firebase features.
View the list of branches for the workshop
git branch -v
To skip this step, do:
git checkout -f 1/firebase-setup
Note that you'll still need the google-services.json file to be placed in app folder.
First thing you'll need is a Firebase project. You can make one here. Then, follow this guide to add Firebase to the app.
To skip this step, do:
git checkout -f 2/authentication
Add these dependencies to app/build.gradle:
compile 'com.google.firebase:firebase-auth:9.4.0'
compile 'com.google.android.gms:play-services-auth:9.4.0'Then, add the sign-in button to activity_signin.xml:
<com.google.android.gms.common.SignInButton
android:id="@+id/sign_in_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:visibility="visible"/>In SignInActivity, declare these two fields:
@BindView(sign_in_button) SignInButtonsignInButton;
privateGoogleApiClientgoogleClient;In the onCreate, build the client and set the OnClickListener:
googleClient = newGoogleApiClient.Builder(this).enableAutoManage(this, this::onConnectionFailed)
.addApi(GOOGLE_SIGN_IN_API, buildSignInOptions())
.build();
signInButton.setOnClickListener(this::onSignIn);Build the sign-in options:
privateGoogleSignInOptionsbuildSignInOptions() {
returnnewGoogleSignInOptions.Builder(DEFAULT_SIGN_IN).requestIdToken(getString(default_web_client_id))
.requestEmail()
.build();
}Display a toast if connection fails:
privatevoidonConnectionFailed(ConnectionResultconnectionResult) {
toast("Google Play Services error.");
}Implement the sign-in logic:
privatevoidsignIn(Intentdata) {
GoogleSignInResultsignInResult = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
if (signInResult.isSuccess()) {
signInTo(signInResult.getSignInAccount());
} else {
toast("Google Sign In failed");
}
}
privatevoidsignInTo(GoogleSignInAccountaccount) {
FirebaseAuth.getInstance()
.signInWithCredential(fromToken(account.getIdToken()))
.addOnCompleteListener(this, this::onSignInComplete);
}
privatevoidonSignInComplete(Task<AuthResult> signinResult) {
if (signinResult.isSuccessful()) {
GoogleUser.saveIfNew();
startActivityForResult(newIntent(this, BoardActivity.class), signOutCode);
} else {
toast("Authentication failed.");
}
}
privateAuthCredentialfromToken(Stringtoken) {
returnGoogleAuthProvider.getCredential(token, null);
}Run the app, you should see the following screen:
After clicking on the Sign In button, the main screen should open:
Now, let's implement sign out.
First, let's make an interface that extends GoogleApiClient.ConnectionCallbacks and implement onConnectionSuspended that will do nothing but log the error:
packagelv.gdgriga.firebase.util;
importandroid.util.Log;
importcom.google.android.gms.common.api.GoogleApiClient;
publicinterfaceConnectionCallbackextendsGoogleApiClient.ConnectionCallbacks {
@OverridedefaultvoidonConnectionSuspended(inti) {
Log.e("Google Api Connection", "suspended.");
}
}Then, let's implement signOut method in SigInActivity:
privatevoidsignOut() {
GoogleUser.signOut();
googleClient.registerConnectionCallbacks((ConnectionCallback) bundle -> {
Auth.GoogleSignInApi.signOut(googleClient);
toast("Signed Out.");
});
googleClient.connect();
}And signOut method in GoogleUser:
staticvoidsignOut() {
auth().signOut();
}
privatestaticFirebaseAuthauth() {
returnFirebaseAuth.getInstance();
}Run the app. On the main screen, press on the grey square in the top right corner and choose the Sign Out item in the popup menu. You should be brought back to the sign in screen and a toast "Signed Out." should appear.
Let's get the signed-in user's avatar. To do that we'll first convert the Firebase user to the domain User. In GoogleUser:
publicstaticUsergetSignedIn() {
returntoDomainUser(getCurrentUser());
}
privatestaticFirebaseUsergetCurrentUser() {
returnauth().getCurrentUser();
}
privatestaticUsertoDomainUser(FirebaseUseruser) {
returnnewUser(user.getDisplayName(), user.getEmail(), getAvatarFrom(user).orElse(null));
}
privatestaticOptional<String> getAvatarFrom(FirebaseUseruser) {
returnOptional.ofNullable(user.getPhotoUrl()).map(Uri::toString);
}One more method to implement is getUserId, we'll need it later:
publicstaticStringgetUserId() {
returngetCurrentUser().getUid();
}The avatar won't appear just yet, but you can still run the app to make sure it still compiles and launches.
To skip this step, do:
git checkout -f 3/database
Add the following dependencies to app/build.gradle:
compile 'com.google.firebase:firebase-database:9.4.0'
compile 'com.firebaseui:firebase-ui-database:0.4.4'Let's start by implementing the TaskViewAdapter which will be responsible for showing our tasks on the board. Make TaskViewAdapter extend FirebaseRecyclerAdapter<Task, TaskViewHolder>:
classTaskViewAdapterextendsFirebaseRecyclerAdapter<Task, TaskViewHolder> {
TaskViewAdapter(Columncolumn) {
super(Task.class, view_task, TaskViewHolder.class, FirebaseDb.getTasksFor(column.name()));
}
...
}Notice the constructor call in which we pass the entity class of our domain object (Task), the layout id for the representation (view_task), the class of the ViewHolder we'll use (find more about view holders here), and and an instance of the query we'll use to retrieve the entities (we'll implement it in a second).
FirebaseRecyclerAdapter defines the populateViewHolder method in which all the magic happens. The only thing you need to do there is to get the entity's key:
StringtaskKey = getRef(position >= getItemCount() ? getItemCount() - 1 : position).getKey();getRef expects an int, which is the position of the element in the list. The selection of the previous to last element is a hack to prevent the ArrayIndexOutOfBounds exception from happening in some cases of fast user actions.
Now, let's implement a query that'll get us all the tasks for the given collection (Backlog, Sprint, In Progress, Done). In FirebasDb:
publicstaticQuerygetTasksFor(StringcollectionName) {
returndb().child(tasks)
.orderByChild(collection).equalTo(collectionName);
}
privatestaticDatabaseReferencedb() {
returnFirebaseDatabase.getInstance().getReference();
}All the connection logic is handled by Firebase itself, all you need is to get the instance reference and start querying.
Next query to implement is getting the task's user by the user's key. Value queries in Firebase are asynchronous, they expect an instance of com.google.firebase.database.ValueEventListener. The ValueEventListener has two methods: onDataChange which is called in case of successful query, and onCancelled which is called if something goes wrong. Let's make our life easier by deriving our own implementation of the ValueEventListener that will allow us to write concise lambda callbacks to use in queries. Change OnSingleValue to look like below:
classOnSingleValueimplementsValueEventListener {
privatefinalConsumer<DataSnapshot> onValue;
OnSingleValue(Consumer<DataSnapshot> onValue) {
this.onValue = onValue;
}
@OverridepublicvoidonDataChange(DataSnapshotdataSnapshot) {
onValue.accept(dataSnapshot);
}
@OverridepublicvoidonCancelled(DatabaseErrordatabaseError) {
Log.e("GDGFirebase", databaseError.getMessage());
}
}Now we can write a query that'll get a user by key. In FirebaseDb:
publicstaticvoidgetUserByKey(Stringkey, Consumer<DataSnapshot> onValue) {
db().child(users).child(key).addListenerForSingleValueEvent(newOnSingleValue(onValue));
}Let's return to TaskViewAdapter to handle the snapshot when it's retrieved from the database. Snapshot are raw data representations returned by Firebase queries. You have to convert them to domain entities before they can be used. You can convert a snapshot to an entity by calling the getValue method and passing in the right class.
privateConsumer<String> setAssigneeAvatar(TaskViewHolderviewHolder) {
returnkey -> FirebaseDb.getUserByKey(key, snapshot -> {
if (!snapshot.exists()) return;
Useruser = snapshot.getValue(User.class);
...
});
}You can check whether the record you searched for exists by calling the exists method on the snapshot.
And inside updateAvatar method in BoardActivity, convert the snapshot to a User:
publicvoidupdateAvatar() {
FirebaseDb.getUserByKey(GoogleUser.getUserId(), snapshot -> {
Useruser = snapshot.getValue(User.class);
...
});
}Now we can implement saveIfNew method in GoogleUser. Inside it we'll check whether the user with given UID exists in the database already and if not, we'll create a new record:
staticvoidsaveIfNew() {
FirebaseUsercurrentUser = getCurrentUser();
FirebaseDb.getUserByKey(currentUser.getUid(), snapshot -> {
if (snapshot.exists()) return;
FirebaseDb.createUser(currentUser.getUid(), toDomainUser(currentUser));
});
}The createUser in FirebaseDb will look like this:
publicstaticvoidcreateUser(Stringuid, Useruser) {
db().child(users).child(uid).setValue(user);
}Launch the app, if all goes well, you should see the user's avatar in the top right corner.
Now let's display the tasks.
In ColumnFragment's onViewCreated, register the RecyclerView.AdapterDataObserver in the TaskViewAdapter. This will make sure that every time a new task is inserted, the application will scroll to it. Then, set this adapter as an adapter for the task list, this will trigger the task population.
@OverridepublicvoidonViewCreated(Viewview, BundlesavedInstanceState) {
...
adapter.registerAdapterDataObserver(observer);
taskList.setAdapter(adapter);
...
}Launch the app, the tasks should show up.
If you try moving the tasks around, the app will crash. Let's fix that by updating the KarmaManager's updateUserKarma method by converting the data snapshot to the User entity.
staticvoidupdateUserKarma(StringuserKey, intdiff) {
FirebaseDb.getUserByKey(userKey, snapshot -> {
Useruser = snapshot.getValue(User.class);
...
});
}Also, implement updateUser in FirebaseDb:
publicstaticvoidupdateUser(StringuserKey, Useruser) {
db().child(users).child(userKey).setValue(user);
}Every time a user moves a task, his karma value is updated. If the task is moved to the right (i.e. being completed), user's karma increases and vice-versa. We'll use this logic later to punish/praise the user for his actions.
Now the tasks can be dragged without a crash, but once the task is released, it appears back where it was. Let's fix that. In FirebaseDb, implement the changeTaskColumn method:
publicstaticvoidchangeTaskColumn(StringdraggedTaskKey, StringnewColumn) {
db().child(tasks).child(draggedTaskKey).child(collection).setValue(newColumn);
}Launch the app and try dragging tasks around. Now they should stick.
Moving tasks around is fun, but let's make it more fun by making editing tasks possible. In FirebaseDb, implement getTaskByKey.
publicstaticvoidgetTaskByKey(StringtaskKey, Consumer<DataSnapshot> onValue) {
db().child(tasks).child(taskKey).addListenerForSingleValueEvent(newOnSingleValue(onValue));
}In EditTaskActivity, convert data snapshot to Task bean.
task = snapshot.getValue(Task.class);We'll also need a list of all users so we can reassign the task. Let's implement getAllUsers in FirebaseDb:
publicstaticvoidgetAllUsers(Consumer<DataSnapshot> onValue) {
db().child(users).orderByValue().addListenerForSingleValueEvent(newOnSingleValue(onValue));
}To make conversion of many users simpler, let's make an utility method. In Snapshot, implement the toUsers method:
publicstaticList<User> toUsers(DataSnapshotdataSnapshot) {
ArrayList<User> users = newArrayList<>();
for (DataSnapshotsnapshot : dataSnapshot.getChildren()) {
Useruser = snapshot.getValue(User.class);
user.setKey(snapshot.getKey());
users.add(user);
}
returnusers;
}Launch the app and touch any task, a dialog will open in which you can edit the task's attributes. You can play around, but when you click apply, the changes won't be persisted.
To persist the changes, implement updateTask in FirebasDb:
publicstaticvoidupdateTask(StringtaskKey, Tasktask) {
db().child(tasks).child(taskKey).setValue(task);
}Let's also implement deleteTask while we're there.
publicstaticvoiddeleteTask(StringtaskKey) {
db().child(tasks).child(taskKey).removeValue();
}Launch the app and try editing/deleting tasks.
We can edit tasks, but why not create? Implement createTask in FirebasDb:
publicstaticvoidcreateTask(Tasktask) {
db().child(tasks).push().setValue(task);
}Launch the app, touch the floating button in the bottom right corner and try creating tasks.
To skip this step, do:
git checkout -f 4/storage
We can create, edit and delete tasks. But wouldn't it be cool if we could add something more to them? How about an image attachment? To do that we'll need a storage. Luckily for us, Firebase comes with one.
Start by adding the storage dependency to app/build.gradle:
compile 'com.google.firebase:firebase-storage:9.4.0'Now we're ready to upload! Implement uploadAttachment and storage methods in Storage class:
publicstaticvoiduploadAttachment(Stringpath, Consumer<String> onSuccess) {
Uriuri = toUri(path);
storage().child(attachments).child(uri.getLastPathSegment()).putFile(uri)
.addOnSuccessListener(snapshot -> {
StorageMetadatameta = snapshot.getMetadata();
onSuccess.accept(toGsLink(meta.getBucket(), meta.getPath()));
})
.addOnFailureListener(error -> error(error.getMessage()));
}
privatestaticStorageReferencestorage() {
returnFirebaseStorage.getInstance().getReference();
}All operations with Firebase storage are performed through an instance reference (just like with FirebaseDatabase). The first call to child selects the folder for our file (which is "attachments"), the second sets the name of the uploaded file (which we get from the URI). toGsLink assembles a URI in Firebase storage format (gs:///). It's shorter than a usual URL and Firebase can work with both formats. putFile then leaves us with an UploadTask that we have to register the success and failure listeners on.
Let's implement attachment downloading. Implement getAttachmentStream and storage(url) in the same Storage class.
publicstaticvoidgetAttachmentStream(Stringurl, Consumer<InputStream> onSuccess) {
storage(url).ifPresent(ref ->
ref.getStream((snapshot, stream) -> {
try {
onSuccess.accept(stream);
stream.close();
} catch (IOExceptione) {
error(e.getMessage());
}
}).addOnFailureListener(error -> error(error.getMessage())));
}
privatestaticOptional<StorageReference> storage(Stringurl) {
try {
returnOptional.of(FirebaseStorage.getInstance().getReferenceFromUrl(url));
} catch (IllegalArgumentExceptione) {
returnOptional.empty();
}
}Getting a reference from a URL is a bit trickier, because we need to account for malformed URLs. That's why we wrap the value in an Optional. If a reference was acquired, we call getStream which accepts an instance of com.google.firebase.storage.StreamDownloadTask.StreamProcessor (we pass it as a lambda). All you need to do here is pass the stream to the onSuccess consumer. And then, preferably, close it.
Lastly, let's implement deletion (which is the easiest of the three). In Storage:
publicstaticvoiddeleteAttachment(Stringurl) {
storage(url).ifPresent(StorageReference::delete);
}Launch the app, and try creating a task with an attachment. Let the attachment load before hitting the apply button. If all goes well, the task will be viewed with the attachment. Edit the task to delete the attachment, it should work as well.
To skip this step, do:
git checkout -f 5/notifications
Firebase Cloud Messaging (FCM) makes notifications simple. You can easily send them to your users based on their device, language, app version, etc.
To enable FCM, you first need to extend com.google.firebase.messaging.FirebaseMessagingService:
packagelv.gdgriga.firebase.notifications;
importcom.google.firebase.messaging.FirebaseMessagingService;
importcom.google.firebase.messaging.RemoteMessage;
publicclassMessagingServiceextendsFirebaseMessagingService {
@OverridepublicvoidonMessageReceived(RemoteMessageremoteMessage) {
super.onMessageReceived(remoteMessage);
}
}In onMessageReceived you're getting an instance of the message for processing, we'll simply delegate the actions to the FirebaseMessagingService.
You can create custom message groups (Topics) and subscribe to them. To do that, extend the com.google.firebase.iid.FirebaseInstanceIdService:
packagelv.gdgriga.firebase.notifications;
importandroid.util.Log;
importcom.google.firebase.iid.FirebaseInstanceId;
importcom.google.firebase.iid.FirebaseInstanceIdService;
importcom.google.firebase.messaging.FirebaseMessaging;
publicclassInstanceIdServiceextendsFirebaseInstanceIdService {
privatestaticfinalStringtopic = "board_updates";
@OverridepublicvoidonTokenRefresh() {
Stringtoken = FirebaseInstanceId.getInstance().getToken();
Log.e("Token", token);
FirebaseMessaging.getInstance().subscribeToTopic(topic);
}
}You'll also need to add the services under manifest's application tag:
<serviceandroid:name=".notifications.MessagingService"android:exported="false">
<intent-filter>
<actionandroid:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
<serviceandroid:name=".notifications.InstanceIdService"android:exported="false">
<intent-filter>
<actionandroid:name="com.google.firebase.INSTANCE_ID_EVENT"/>
</intent-filter>
</service>Launch the app and put it in background. Go to the project's console and send the notification to your device.
To skip this step, do:
git checkout -f 6/remote-config
Firebase allows your app to have a remotely-stored configuration. In the Remote Config section of the console create a parameter with name toolbar_color and set it to whichever color you like (#ffd25a, for example).
We'll implement all the necessary logic in the RemoteConfig class.
packagelv.gdgriga.firebase.remote_config;
importandroid.util.Log;
importcom.google.firebase.remoteconfig.FirebaseRemoteConfig;
importcom.google.firebase.remoteconfig.FirebaseRemoteConfigSettings;
importjava.util.HashMap;
importjava8.util.function.Consumer;
publicfinalclassRemoteConfig {
privatefinalFirebaseRemoteConfigconfig;
privateRemoteConfig() {
this.config = FirebaseRemoteConfig.getInstance();
setup();
}
publicstaticvoidfetchConfig(Consumer<FirebaseRemoteConfig> onSuccess) {
newRemoteConfig().fetch(onSuccess);
}
privatevoidsetup() {
FirebaseRemoteConfigSettingssettings = newFirebaseRemoteConfigSettings.Builder()
.setDeveloperModeEnabled(true)
.build();
config.setConfigSettings(settings);
config.setDefaults(newHashMap<String, Object>() {{
put("toolbar_color", "#303F9F");
}});
}
privatevoidfetch(Consumer<FirebaseRemoteConfig> onSuccess) {
config.fetch(cacheExpiration())
.addOnSuccessListener(nothing -> {
config.activateFetched();
onSuccess.accept(config);
})
.addOnFailureListener(error -> {
Log.e(getClass().getName(), error.getMessage());
onSuccess.accept(config);
});
}
privatelongcacheExpiration() {
returnconfig.getInfo().getConfigSettings().isDeveloperModeEnabled() ? 0 : 3600;
}
}All the operations happen through an instance of FirebaseRemoteConfig. First you need to set it up by providing the settings which you build with the appropriate builder. Then you need to set the defaults in case values couldn't be fetched. Finally, when actually fetching the configuration, you need to set the cache expiration time and add the listeners for success and failure cases. After the data was fetched, we have to activate it.
Let's make it possible for the user to trigger configuration fetching by clicking on the section in the popup menu.
casefetch_config:
RemoteConfig.fetchConfig(config ->
toolbar.setBackgroundColor(parseColor(config.getString("toolbar_color"))));
break;Launch the app and choose the Fetch Configuration option from the menu. The color of the toolbar should change.
To skip this step, do:
git checkout -f 7/install-invites
Firebase provides the possibility to send install invitations.
app/build.gradle:
compile'com.google.android.gms:play-services-appinvite:9.4.0'Inside the onCreate method of the InvitationsActivity build the invitations API and register the connection callback on it. When the API connection happens, the sendInvitation method will be invoked. Inside it, build the intent to send an invitation at start an activity for result (this same activity):
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
GoogleApiClientclient = newGoogleApiClient.Builder(this)
.enableAutoManage(this, connectionResult ->
Log.e(INVITATIONS, "Connection failed: " + connectionResult))
.addApi(Auth.GOOGLE_SIGN_IN_API).addApi(AppInvite.API)
.build();
client.registerConnectionCallbacks((ConnectionCallback) bundle -> sendInvitation());
}
privatevoidsendInvitation() {
Intentinvite = newAppInviteInvitation.IntentBuilder(getString(lets_shake_hands))
.setMessage(getString(baby_lets_shake_hands))
.setCallToActionText(getString(lets_be_friends))
.build();
startActivityForResult(invite, REQUEST_INVITE);
}Inside the onActivityResult we'll just log the number of invitations sent. To do that, we'll retrieve invitation ids from the result data.
String[] invitationIds = AppInviteInvitation.getInvitationIds(resultCode, data);Launch the app and touch the popup menu's Send Invitation entry. Try to invite a friend to install the app (he won't be able to, because your app won't be published to Google Play).
To skip this step, do:
git checkout -f 8/analytics
compile 'com.google.firebase:firebase-analytics:9.4.0'Analytics in Firebase work from the box. But if you want to log custom events to track user navigation through the app, for example, you can do that too.
Inside Analytics.userOpenedApp, do:
FirebaseAnalytics.getInstance(context).logEvent(FirebaseAnalytics.Event.APP_OPEN, newBundle());This will log an event which you should be able to see on the analytics tab in your Firebase Console.
To skip this step, do:
git checkout -f 9/crashes
compile 'com.google.firebase:firebase-crash:9.4.0'This one is pretty straightforward. Firebase will log all the exceptions that happened in your app with their stacktraces.
Let's cause the app to crash by throwing an exception when the Crash and Burn option is selected from the popup menu.
casecrash_menu:
thrownewMotherKaliStartedPartyDarklyException("Just checkin'");To to the Firebase Console and see that your exception was recorded.
To skip this step, do:
git checkout -f 10/ads
It's that time when you want to start earning money. Who doesn't love ads? Huge profits are just around the corner.
compile 'com.google.android.gms:play-services-ads:9.4.0'In activity_signin.xml, declare the ads widget:
...
<FrameLayoutxmlns:ads="http://schemas.android.com/apk/res-auto"
...>
...
<com.google.android.gms.ads.AdView
android:id="@+id/ad_view"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_gravity="bottom"ads:adSize="BANNER"ads:adUnitId="@string/banner_ad_unit_id"/>
</FrameLayout>Bind it as a field in the SigninActivity
@BindView(ad_view) AdViewadView;Then load it inside the onCreate:
adView.loadAd(newAdRequest.Builder().build());And integrate it with the SigninActivity's lifecycle:
@OverrideprotectedvoidonPause() {
if (adView != null) adView.pause();
super.onPause();
}
@OverrideprotectedvoidonResume() {
super.onResume();
if (adView != null) adView.resume();
}
@OverrideprotectedvoidonDestroy() {
if (adView != null) adView.destroy();
super.onDestroy();
}Launch the app and behold the ads!
Congratulations, you're done. Hope you enjoyed the event. We'll be happy to see you at the next one!










