Ein Notizbuch mit Firebase Cloud Backend für Android Apps und webbasierten Zugriff

1. Aufgabe

Firebase ist eine neue Technologie von Google, sie ist unter der Rubrik BaaSBackend-as-a-Service – einzuordnen. Zahlreiche Tools und APIs stehen in dieser Infrastruktur zur Verfügung, die speziell für mobile Apps konzipiert sind und mit einer ganzen Reihe von Services wie Authentication, Realtime Database, Cloud Messaging, Storage, Hosting, Test Lab, Crash Reporting, etc. den Entwickler in der Realisierung von Mobile Apps unterstützen. Es handelt sich bei diesem Tutorial um einen ersten Einstieg in die Thematik Backend-as-a-Service Wir gehen deshalb nur auf die zwei Teilaspekte Authentication und Realtime Database näher ein.

Wir betrachten den ersten Einstieg in die Firebase-Technologie am Beispiel eines Notizbuchs. Die Notizen aller Benutzer werden dabei auf dem Firebase-Backend in einer dem App-Projekt zugeordneten NoSql-Datenbank abgelegt (Realtime Database Service). Ein authentifizierter Benutzer kann in der App Notizen eintippen und betrachten. Natürlich dürfen für diesen Benutzer nur seine persönlichen Notizen sichtbar sein. Mit den beiden Firebase-Services Realtime-Database und Authentifizierung lässt sich diese Anforderung einfach und elegant umsetzen.

Die Firebase-Technologie ist für Clients unterschiedlicher Plattformen ausgelegt. iOS-, Android- als auch web-basierte Apps können aus mehreren Programmiersprachen-Bindings (Java, JavaScript, Swift, und weitere) auswählen, auf welche Weise sie das Firebase-Backend-as-a-Service-API in seiner Eigenschaft als RESTful Service ansprechen wollen. In unserer Fallstudie finden Sie die Implementierung einer Android-App und einer web-basierten App vor. Hintergrund dieser Entwurfsentscheidung ist die Neugierde, sowohl in Java (Android-App) als auch mit JavaScript (plus HTML/CSS) zwei unterschiedliche Clients für dasselbe Firebase-Projekt betrachten zu wollen.

In knappe Worte gefasst lautet die Aufgabenbeschreibung dieser Fallstudie wie folgt: Erstellen Sie eine App, die es dem Benutzer ermöglicht, persönliche Notizen in einer App einzugeben, zu löschen und alle seine Notizen übersichtsartig darzustellen. Der Clou der App liegt natürlich darin, dass der Benutzer seine Notizen über beliebig viele Smartphones hinweg synchronisiert vorfindet. Zu diesem Zweck muss der Benutzer sich vor Gebrauch der App einloggen oder bei der erstmaligen Bedienung registrieren. Fragen zur Gestaltung des App User-Interfaces beantworten am besten Abbildung 1, Abbildung 2 und Abbildung 3:

Startseite der Android-App – hier: Sign Up-Maske.

Abbildung 1. Startseite der Android-App – hier: Sign Up-Maske.


Startseite der Android-App – hier: Sign In-Maske.

Abbildung 2. Startseite der Android-App – hier: Sign In-Maske.


Auf der Startseite der App kann sich ein Benutzer in der App registrieren („Sign Up“-Modus – siehe Abbildung 1) oder anmelden („Sign In“-Modus – siehe Abbildung 2). Das Firebase-Portal unterstützt eine Reihe von Authentifizierungs-Methoden (Google, Twitter, Facebook, usw.). In dieser Fallstudie kommt die Anmeldemethode „E-Mail-Adresse/Passwort“ zum Einsatz. Ist man eingeloggt, wechselt die Android-App zu einer zweiten Activity (Abbildung 3). Hier kann man im oberen Teil die Notizenliste des eingeloggten Benutzers betrachten. Im unteren Teil besteht die Möglichkeit, weitere Notizen interaktiv einzugeben.

Darstellung der Notizen eines Benutzers / Eingabe von Notizen.

Abbildung 3. Darstellung der Notizen eines Benutzers / Eingabe von Notizen.


Rein aus Aufwandsgründen ist die Android-App in der Funktionalität etwas schmal geraten. Ein Benutzer kann nur seine Notizen betrachten oder neue Notizen ergänzen. Zum Löschen seiner Notizen muss der Benutzer auf die Web-App zurückgreifen. Mit ihr lassen sich neben der Anzeige aller Notizen weitere Notizen hinzufügen oder vorhandene Notizen löschen. Abbildung 4 zeigt die „Sign In“-Oberfläche der Web-Anwendung.

Sign In-Maske der Web-App.

Abbildung 4. „Sign In“-Maske der Web-App.


Nach dem Einloggen befindet sich der Benutzer in einer Oberfläche wie in Abbildung 5 gezeigt:

Web-App: Darstellung der Notizen eines Benutzers.

Abbildung 5. Web-App: Darstellung der Notizen eines Benutzers.


Rechts oben in Abbildung 5 erkennt man in der Menü-Leiste Ikonen zum Aktualisieren der Notizen-Liste sowie zum Hinzufügen oder Löschen von Notizen. Beim Klick auf das „Löschen“-Symbol werden die einzelnen Notizen in der Ansicht um Auswahlkästchen (engl. CheckBox) ergänzt – siehe Abbildung 6. Alles weitere zur Umsetzung dieser Aufgabenstellung in Java- oder HTML/JavaScript-Code entnehmen Sie bitte dem Lösungsteil.

Web-App: Modifizierte Darstellung der Notizenliste zum Löschen einer oder mehrerer Notizen.

Abbildung 6. Web-App: Modifizierte Darstellung der Notizenliste zum Löschen einer oder mehrerer Notizen.

2. Lösung

Quellcode: Siehe auch github.com/peterloos/AnotherToDoApp_Android.git und github.com/peterloos/AnotherToDoApp_Web.git.

Die komplette Fallstudie ist vom Umfang her gesehen nicht gerade klein geraten. Schwerpunkte in der Betrachtung der Realisierung möchte ich deshalb auf Themenbereichen

  • Anlegen eines Projekts im Firebase-Portal,

  • Authentifizierung eines Benutzers mit Java bzw. JavaScript sowie

  • Zugriff auf die Realtime-Datenbank mit Java bzw. JavaScript

widmen.

Die folgenden Erläuterungen stellen in gewisser Weise einen Ausschnitt auf der offiziellen Dokumentation von Firebase dar. Nichtsdestotrotz wollte ich dieses Tutorial in sich abgeschlossen halten und beginne deshalb mit einigen allgemeinen Hilfestellungen zum Einstieg in das Firebase-Portal. Bevor man prinzipiell mit Firebase starten kann, muss man sich auf der Firebase-WebSite firebase.google.com einen Account anlegen (Abbildung 7):

Ausschnitt aus der Startseite des Firebase Portals.

Abbildung 7. Ausschnitt aus der Startseite des Firebase Portals.


Möchte man ein Firebase-Projekt anlegen, führt der Weg – siehe in Abbildung 7 rechts oben – zur „Firebase Konsole“. Dort findet man – sofern vorhanden – die bereits angelegten Projekte des Entwicklers übersichtsartig dargestellt vor und kann mit dem Dialog aus Abbildung 8 ein neues Projekt anlegen:

Anlegen eines neuen Projekts im Firebase Portal.

Abbildung 8. Anlegen eines neuen Projekts im Firebase Portal.


Beim Anlegen eines Projekts im Firebase Portal legt man sich noch nicht auf eine bestimmte Technologie in Bezug auf die Client-Anwendung fest. Ganz im Gegenteil: Die Methoden des Firebase SDK sind über ein RESTful API erreichbar und damit prinzipiell programmiersprachenunabhängig. Zahlreiche Bindings, zum Beispiel für Java oder JavaScript, vereinfachen den Zugriff auf das Firebase-Projekt für Client-Anwendungen wie etwa eine Android-App (Programmiersprache Java) oder eine web-basierte HTML5-Anwendung (Programmiersprache JavaScript).

Für das serverseitige Firebase-Projekt ist beim Anlegen nur ein Name, eine Projekt-ID (optional) und eine Land/Regionen-Zuordnung vorzunehmen (Abbildung 9). Auch bei der dritten Einstellung kann man es bei der Voreinstellung „Vereinigte Staaten“ belassen. Wechselt man mit seinem Projekt von der Testphase in einen (zahlungspflichtigen) Produktionsbetrieb, dann würde sich hier die korrekte Landeseinstellung anbieten, da lt. Google-Dokumentation hierdurch „die entsprechende Währung für Umsatzberichte“ festgelegt wird.

Erstellen eines Projekts im Firebase Portal: Vergabe eines Projektnamens.

Abbildung 9. Erstellen eines Projekts im Firebase Portal: Vergabe eines Projektnamens.


Wir nähern uns Schritt für Schritt unseren Client-Anwendungen. In Abbildung 10 müssen wir uns deshalb konkret zwischen einer iOS-App, einer Android-App und einer Web-App entscheiden:

Auswahl der Anwendungstechnologie.

Abbildung 10. Auswahl der Anwendungstechnologie.


Damit eine Firebase-Client-Anwendung den Zugang zum ihrem zugeordneten serverseitigem Firebase-Projekt findet, baut diese an Hand einer Reihe von Endpunkten (Project-ID, API-Key, Auth-Domain usw.) eine Kommunikationsstrecke auf. Diese Informationen sind für jeden client-seitigen Anwendungstyp identisch, nur in einer unterschiedlichen Notation bereitzustellen. Bei Android-Anwendungen handelt es sich hierbei um eine Datei im JSON-Format, die dem Projekt hinzufügen ist (siehe dazu auch Abbildung 12). Bei einer Web-Anwendung ist im BOPY-Markup Tag ein minimalistisches JavaScript-Codefragment mit denselben Informationen zu ergänzen.

Im Falle von Android können wir das JSON-Objekt mit einem Editor ansehen. Die Datei trägt den Namen google-services.json:

{
  "project_info" : {
    "project_number" : "516966698708",
    "firebase_url" : "https://anothertodoapp.firebaseio.com",
    "project_id" : "anothertodoapp",
    "storage_bucket" : "anothertodoapp.appspot.com"
  },
  "client" : [
    {
      "client_info" : {
        "mobilesdk_app_id" : "1:516966698708:android:e99550a50c284c03",
        "android_client_info" : {
          "package_name" : "de.peterloos.anothertodoapp"
        }
      },
      "oauth_client" : [
        {
          "client_id" : "516966698708-80b1e7tmei1b9.......apps.googleusercontent.com",
          "client_type" : 3
        }
      ],
      "api_key" : [
        {
          "current_key" : "AIzaSyC2xKr3igCoW............"
        }
      ],
      ...
    }
  ],
  "configuration_version" : "1"
}

Im Falle einer Android-App ist in dieser JSON-Datei auch der Paketname (der obersten Ebene) einzupflegen, in dessen Namensraum die Java-Klassen abgelegt sind. Alle anderen Einträge sind optional, wie wir in Abbildung 11 erkennen können:

Firebase zu einer Android-App hinzufügen.

Abbildung 11. Firebase zu einer Android-App hinzufügen.


Die zuvor erwähnte JSON-Datei ist in einem Android-Projekt an einer bestimmten Stelle im Verzeichnisbaum abzulegen. Hilfestellung hierzu leistet eine Folgemaske von Abbildung 11, die wir in Abbildung 12 vorfinden:

Firebase zu einer Android-App hinzufügen - zum Zweiten.

Abbildung 12. Firebase zu einer Android-App hinzufügen - zum Zweiten.


Die build.gradle-Dateien des Android-Projekts – wir finden in der Verzeichnisstruktur gleich zwei Dateien dieses Namens von – müssen ebenfalls angefasst bzw. erweitert werden. Das Android-APK File ist um ein Google Services Plug-In zu erweitern. Entsprechende Festlegungen sind penibel genau vorzunehmen. Details hierzu findet man wiederum im Firebase-Portal vor (Abbildung 13):

Anpassungen der build.gradle-Dateien des Android-Projekts.

Abbildung 13. Anpassungen der build.gradle-Dateien des Android-Projekts.


Im Falle eine Web-Anwendung müssen wir jede HTML-Datei, die auf das Firebase-Portal zugreift, um einen entsprechenden Initialisierungscode erweitern. Dieses JavaScript-Codefragment wird im Portal der Firebase-Anwendung automatisch generiert; mit Copy-Paste müssen wir den Code am Ende des BODY-Markup-Elements einfügen (Abbildung 14):

JavaScript-Codefragment zur Initialisierung des Zugriffs auf Firebase.

Abbildung 14. JavaScript-Codefragment zur Initialisierung des Zugriffs auf Firebase.


Wir verlassen nun das Firebase-Portal und kommen auf den Quellcode der Android-App zu sprechen. Den gesamten Quellcode der App finden Sie unter github.com/peterloos/AnotherToDoApp_Android. In den nachfolgenden Betrachtungen wollen wir etwas näher auf die Authentifizierung eines Benutzers eingehen. Den Einstiegspunkt einer jeden Android-App (eine Aktivität namens MainActivity) finden wir in Listing 1 vor. Prinzipiell könnte man diese Aktivität auch umbenennen, aber der Einfachheit halber habe ich es bei den Vorschlägen des Android Studios an dieser Stelle belassen:

001: package de.peterloos.anothertodoapp;
002: 
003: import android.content.Intent;
004: import android.os.Bundle;
005: import android.support.v7.app.AppCompatActivity;
006: import android.view.Menu;
007: import android.view.MenuItem;
008: import android.view.View;
009: import android.widget.ArrayAdapter;
010: import android.widget.Button;
011: import android.widget.EditText;
012: import android.widget.ListView;
013: import android.widget.Toast;
014: 
015: import com.google.firebase.auth.FirebaseAuth;
016: import com.google.firebase.auth.FirebaseUser;
017: import com.google.firebase.database.ChildEventListener;
018: import com.google.firebase.database.DataSnapshot;
019: import com.google.firebase.database.DatabaseError;
020: import com.google.firebase.database.DatabaseReference;
021: import com.google.firebase.database.FirebaseDatabase;
022: 
023: public class MainActivity extends AppCompatActivity
024:         implements View.OnClickListener, ChildEventListener {
025: 
026:     private FirebaseAuth firebaseAuth;
027:     private FirebaseUser firebaseUser;
028:     private DatabaseReference databaseRef;
029:     private DatabaseReference itemsRef;
030:     private String userId;
031: 
032:     private EditText edittextNewItem;
033:     private Button buttonAdd;
034:     private ListView listviewItems;
035:     private ArrayAdapter<String> adapter;
036: 
037:     @Override
038:     protected void onCreate(Bundle savedInstanceState) {
039:         super.onCreate(savedInstanceState);
040:         this.setContentView(R.layout.activity_main);
041: 
042:         // retrieve UI controls
043:         this.edittextNewItem = (EditText) this.findViewById(R.id.todoText);
044:         this.buttonAdd = (Button) this.findViewById(R.id.addButton);
045:         this.listviewItems = (ListView) this.findViewById(R.id.listviewItems);
046: 
047:         // connect UI controls with event handlers / adapters
048:         this.buttonAdd.setOnClickListener(this);
049:         this.adapter = new ArrayAdapter<>(
050:                 this, android.R.layout.simple_list_item_1, android.R.id.text1);
051:         this.listviewItems.setAdapter(adapter);
052: 
053:         // initialize Firebase Auth and RealTime Database
054:         this.firebaseAuth = FirebaseAuth.getInstance();
055:         this.firebaseUser = this.firebaseAuth.getCurrentUser();
056:         this.databaseRef = FirebaseDatabase.getInstance().getReference();
057: 
058:         if (this.firebaseUser == null) {
059: 
060:             this.loadLogInView();  // not logged in, launch LogIn activity
061:         } else {
062: 
063:             // retrieve child node of realtime database
064:             this.userId = firebaseUser.getUid();
065:             this.itemsRef =
066:                     this.databaseRef.child("users").child(this.userId).child("items");
067:             this.itemsRef.addChildEventListener(this);
068:         }
069:     }
070: 
071:     @Override
072:     public boolean onCreateOptionsMenu(Menu menu) {
073: 
074:         // inflate the menu
075:         this.getMenuInflater().inflate(R.menu.menu_main, menu);
076:         return true;
077:     }
078: 
079:     @Override
080:     public boolean onOptionsItemSelected(MenuItem item) {
081: 
082:         int id = item.getItemId();
083:         if (id == R.id.action_logout) {
084:             this.firebaseAuth.signOut();
085:             this.loadLogInView();
086:         }
087: 
088:         return super.onOptionsItemSelected(item);
089:     }
090: 
091:     /*
092:      * implementation of interface 'View.OnClickListener'
093:      */
094: 
095:     @Override
096:     public void onClick(View view) {
097: 
098:         String input = this.edittextNewItem.getText().toString();
099:         if (input.isEmpty()) {
100:             Toast.makeText(
101:                     MainActivity.this, R.string.error_empty_input,
102:                     Toast.LENGTH_LONG).show();
103:             return;
104:         }
105: 
106:         DatabaseReference newChild = this.itemsRef.push().child("title");
107:         newChild.setValue(input);
108: 
109:         // clear UI
110:         this.edittextNewItem.setText("");
111:     }
112: 
113:     /*
114:      * implementation of interface 'ChildEventListener'
115:      */
116: 
117:     @Override
118:     public void onChildAdded(DataSnapshot dataSnapshot, String s) {
119: 
120:         DataSnapshot childSnapshot = dataSnapshot.child("title");
121:         String title = (String) childSnapshot.getValue();
122:         this.adapter.add(title);
123:     }
124: 
125:     @Override
126:     public void onChildRemoved(DataSnapshot dataSnapshot) {
127: 
128:         DataSnapshot childSnapshot = dataSnapshot.child("title");
129:         String title = (String) childSnapshot.getValue();
130:         this.adapter.remove(title);
131:     }
132: 
133:     @Override
134:     public void onChildChanged(DataSnapshot dataSnapshot, String s) {
135:     }
136: 
137:     @Override
138:     public void onChildMoved(DataSnapshot dataSnapshot, String s) {
139:     }
140: 
141:     @Override
142:     public void onCancelled(DatabaseError databaseError) {
143:     }
144: 
145:     // private helper methods
146:     private void loadLogInView() {
147: 
148:         Intent intent = new Intent(this, LogInActivity.class);
149:         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
150:         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
151:         this.startActivity(intent);
152:     }
153: }

Beispiel 1. Klasse MainActivity: Haupteinstiegspunkt der Android-App.


Eine erste Schlüsselstelle finden wir in Listing 1 in Zeile 55 vor. Bevor die Hauptaktivität der App sichtbar wird – also den ihr durch Zeile 40 zugeordneten visuellen Inhalt R.layout.activity_main darstellt –, wird in Zeile 55 durch den Aufruf von getCurrentUser an einem FirebaseAuth-Objekt der aktuelle Firebase-Benutzer ermittelt. Gibt es diesen nicht – es ist also kein Benutzer an der App eingeloggt –, verzweigt diese zur Laufzeit in die Methode loadLogInView (Zeile 146). Dort wird die aktuelle Task – in anderen Worten: die noch nicht sichtbar gewordene Hauptaktivität – beendet und eine alternative, zweite Aktivität LogInActivity gestartet.

Auf die Mechanismen des Sign-In und Sign-Up gehen wir etwas weiter unten näher ein. Haben wir es hingegen mit einem angemeldeten Firebase-Benutzer zu tun, verzweigen wir in die Zeilen 64ff. Dort geht es mit der Realtime-Database Funktionalität von Firebase weiter. Es wird an Hand der Benutzer-Id ein Pfad gebildet, zum Beispiel users/BbSZD3KqT8QkywIzRsTHdN3oBCF2/items, und darauf aufbauend ein DatabaseReference-Objekt erzeugt. An diesem Objekt – sprich: an dieser Position in der hierarchischen NoSql-Datenstruktur – kann man sich für Änderungen anmelden, siehe den addChildEventListener-Aufruf von Zeile 67. Das Wort „Änderung“ ist hier wie folgt zu verstehen: Die onChildAdded-Methode wird zuerst für jedes existierende (also schon vorhandene) Kind-Element aufgerufen, und dann später pro neuem Kind-Element, dass an der Stelle des spezifizierten Pfads hinzugefügt wird.

Im vorliegenden Quellcode von Listing 1 habe ich die Methoden onChildAdded und onChildRemoved der Firebase-ChildEventListener-Schnittstelle überschrieben. Dies taugt für einen ersten, einfachen Ansatz, um eine Listen-Ansicht bzgl. Hinzufügen und Löschen von Einträgen auf dem Laufenden zu halten. In einer verbesserten App müsste man auch den nicht implementierten Methoden, vor allem onChildChanged und onChildMoved, sein Augenmerk widmen, aber diese Anmerkung nur der Vollständigkeit halber. Der ganze Rest des Quellcodes aus Listing 1 lässt sich im weitesten Sinne als Routine-Quellcode ansehen (Einbindung eines Menüs, Anlegen eines ArrayAdapter-Objekts zum Füllen eines ListView-Steuerelements, Verknüpfung der Android-Steuerelemente mit ihren korrespondierenden Java-Objekten, usw.), wir kommen deshalb in Listing 2 gleich auf die nächste Aktivität LogInActivity zu sprechen:

01: package de.peterloos.anothertodoapp;
02: 
03: import android.content.Intent;
04: import android.os.Bundle;
05: import android.support.annotation.NonNull;
06: import android.support.v7.app.AlertDialog;
07: import android.support.v7.app.AppCompatActivity;
08: import android.view.View;
09: import android.widget.Button;
10: import android.widget.EditText;
11: import android.widget.TextView;
12: 
13: import com.google.android.gms.tasks.OnCompleteListener;
14: import com.google.android.gms.tasks.Task;
15: import com.google.firebase.auth.AuthResult;
16: import com.google.firebase.auth.FirebaseAuth;
17: 
18: public class LogInActivity extends AppCompatActivity implements View.OnClickListener {
19: 
20:     protected EditText edittextEmail;
21:     protected EditText edittextPassword;
22:     protected Button buttonLogIn;
23:     protected TextView textviewSignUp;
24:     private FirebaseAuth firebaseAuth;
25: 
26:     @Override
27:     protected void onCreate(Bundle savedInstanceState) {
28:         super.onCreate(savedInstanceState);
29:         this.setContentView(R.layout.activity_log_in);
30: 
31:         // initialize firebase auth
32:         this.firebaseAuth = FirebaseAuth.getInstance();
33: 
34:         // retrieve references of controls
35:         this.edittextEmail = (EditText) this.findViewById(R.id.emailField);
36:         this.edittextPassword = (EditText) this.findViewById(R.id.passwordField);
37:         this.buttonLogIn = (Button) this.findViewById(R.id.loginButton);
38:         this.textviewSignUp = (TextView) this.findViewById(R.id.signUpText);
39: 
40:         // connect controls with event listener
41:         this.buttonLogIn.setOnClickListener(this);
42:         this.textviewSignUp.setOnClickListener(this);
43:     }
44: 
45:     @Override
46:     public void onClick(View view) {
47: 
48:         if (view == this.textviewSignUp) {
49: 
50:             Intent intent = new Intent(this, SignUpActivity.class);
51:             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
52:             intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
53:             this.startActivity(intent);
54: 
55:         } else if (view == this.buttonLogIn) {
56: 
57:             String email = LogInActivity.this.edittextEmail.getText().toString();
58:             String password = LogInActivity.this.edittextPassword.getText().toString();
59:             email = email.trim();
60:             password = password.trim();
61: 
62:             if (email.isEmpty() || password.isEmpty()) {
63:                 AlertDialog.Builder builder = new AlertDialog.Builder(LogInActivity.this);
64:                 builder.setMessage(R.string.login_error_message)
65:                     .setTitle(R.string.login_error_title)
66:                     .setPositiveButton(android.R.string.ok, null);
67:                 AlertDialog dialog = builder.create();
68:                 dialog.show();
69:             } else {
70:                 Task<AuthResult> task =
71:                         this.firebaseAuth.signInWithEmailAndPassword(email, password);
72: 
73:                 task.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
74:                     @Override
75:                     public void onComplete(@NonNull Task<AuthResult> task) {
76:                         if (task.isSuccessful()) {
77:                             Intent intent =
78:                                     new Intent(LogInActivity.this, MainActivity.class);
79:                             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
80:                             intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
81:                             LogInActivity.this.startActivity(intent);
82:                         } else {
83:                             AlertDialog.Builder builder =
84:                                     new AlertDialog.Builder(LogInActivity.this);
85:                             builder.setMessage(task.getException().getMessage())
86:                                 .setTitle(R.string.login_error_title)
87:                                 .setPositiveButton(android.R.string.ok, null);
88:                             AlertDialog dialog = builder.create();
89:                             dialog.show();
90:                         }
91:                     }
92:                 });
93:             }
94:         }
95:     }
96: }

Beispiel 2. Klasse LogInActivity: Sign-In eines Firebase-Benutzers.


Die zentrale Stelle von Listing 2 finden wir in Zeile 71 vor: Die Methode signInWithEmailAndPassword an einem FirebaseAuth-Objekt wickelt den kompletten Anmeldevorgang eines Benutzers an diesem Projekt ab. Programmiertechnisch ist zu beachten, dass diese Methode (wie auch zahlreiche andere Methoden des Firebase API) asynchron arbeitet. Resultate dieses Methodenaufrufs werden ereignisgesteuert übermittelt. Man ist folglich gezwungen, am Rückgabewert des signInWithEmailAndPassword-Methodenaufrufs, einem Task<AuthResult>-Objekt, sich mit einer Implementierung der OnCompleteListener-Schnittstelle anzumelden – siehe Aufruf von addOnCompleteListener in Zeile 73.

Die verbleibenden Zeilen bewirken bei erfolgreicher Anmeldung einen Wechsel zur MainActivity der App – diese haben wir in Listing 1 schon vorgestellt. Andernfalls verbleiben wir mit einer entsprechenden Fehlermeldung in der LogInActivity der App und der Zugang zur MainActivity bleibt uns auf Grund der fehlenden Authentifizierung verwehrt.

Die Gestaltung des Registrierens an dieser App ist sehr, sehr ähnlich zum Quellcode von Listing 2. Die zentrale Methode für den Anmeldevorgang heißt dieses Mal createUserWithEmailAndPassword. In ihrer technischen Handhabung (asynchroner Methodenaufruf) ist sie sehr ähnlich zur Methode signInWithEmailAndPassword gehalten. Ich verweise an dieser Stelle auf das Online-Material bei firebase.google.com/docs/.

Abschließend soll für den Themenbereich „Authentifizierung“ nicht unerwähnt bleiben, dass neben dem Anmeldeverfahren „EMail-Adresse/Passwort“ eine ganze Reihe weiterer Anmeldemethoden unterstützt werden. In Abbildung 15 finden Sie dazu einen Überblick:

Unterschiedliche Anmeldemethoden für ein Firebase-Projekt.

Abbildung 15. Unterschiedliche Anmeldemethoden für ein Firebase-Projekt.


Und damit kommen wir auch schon zur Web-App dieses Projekts. Für reale Firebase-Projekte könnte der web-basierte Zugang beispielsweise den Status einer „Administrations“-Anwendung haben. Also den lesenden und schreibenden Zugriff auf alle Daten eines Benutzers. In der mobilen App hingegen kann man den Datenbestand nur lesend betrachten. Software zum Online-Banking beispielsweise unterscheidet – falls der Benutzer es so möchte – zwischen einer so genannten „Kontostands-App“ für mobile Devices auf der einen Seite und dem regulären Online-Banking mit allen Funktionen auf einem PC (Web-Anwendung) auf der anderen Seite.

Für die Gestaltung der Webseiten-Oberfläche kommt Material Design Lite zum Einsatz, kurz auch MDL genannt. MDL basiert auf HTML, CSS sowie JavaScript und vermeidet weitestgehend weitere Abhängigkeiten, um einfach installierbar und nutzbar zu sein. Die Bibliothek versieht statische Webseiten mit dem typischen Look and Feel von Material Design. Diese Designsprache führte Google mit Android 5.0 Lollipop ein und strebte damit eine einheitliche Optik für mobile und Desktop Anwendungen an.

Wie bei der Android-App haben wir es auch bei der Web Anwendung gewissermaßen mit zwei unterschiedlichen Abschnitten im Quellcode zu tun: An die Stelle von Java tritt JavaScript (Wechsel der Programmiersprache). Für die Beschreibung der Oberfläche setzen wir jetzt HTML (und einige wenige Elemente von CSS) an Stelle der Android-proprietären XML-Beschreibungssprache für App-Oberflächen ein. Bzgl. der Funktionalität des Programms gilt es, die Authentifizierung eines Benutzers und die anschließende Visualisierung seines Datenbestands umzusetzen. In Listing 3 finden Sie den Einstiegspunkt der Web-Anwendung (Datei index.html) vor, es wird die Oberfläche auf Basis von MDL realisiert:

01: <!DOCTYPE html>
02: 
03: <html>
04:     <head>
05:         <title>Another ToDo Application</title>
06: 
07:         <!-- Material Design Lite -->
08:         <script src="https://code.getmdl.io/1.3.0/material.min.js"></script>
09:         <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.blue_grey-yellow.min.css" />
10: 
11:         <!-- Material Design icon font -->
12:         <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Roboto:400,100,500,300italic,500italic,700italic,900,300">
13:         <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
14: 
15:         <link rel="stylesheet" href="/style.css">
16:     </head>
17: 
18:     <body>
19:         <div class="mdl-layout mdl-js-layout">
20:             <header class="mdl-layout__header">
21:                 <div class="mdl-layout__header-row">
22:                     <span class="mdl-layout__title">Another Todo-List Web App</span>
23:                     <div class="mdl-layout-spacer"></div>
24:                     <nav class="mdl-navigation">
25:                         <a id="navLinkSignUp" class="mdl-navigation__link" href="#">Sign Up</a>
26:                         <a id="navLinkSignIn" class="mdl-navigation__link" href="#">Sign In</a>
27:                     </nav>
28:                 </div>
29:             </header>
30: 
31:             <main class="mdl-layout__content  main_content">
32: 
33:                 <h3>Another ToDo-List Application</h3>
34: 
35:                 <div class="login-form-div mdl-grid mdl-shadow--2dp">
36: 
37:                     <p id="errorMessage"></p>
38: 
39:                     <div class="mdl-cell mdl-cell--12-col cell_con">
40:                         <i class="material-icons margin-left-from-icon">account_circle</i>
41:                         <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
42:                             <input class="mdl-textfield__input" type="text" pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$" id="txtEmail">
43:                             <label class="mdl-textfield__label" for="txtEmail">Enter valid Email</label>
44:                             <span class="mdl-textfield__error">Invalid Email...!</span>
45:                         </div>
46:                     </div>
47: 
48:                     <div class="mdl-cell mdl-cell--12-col cell_con">
49:                         <i class="material-icons margin-left-from-icon">lock</i>
50:                         <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
51:                             <input class="mdl-textfield__input" type="text" id="txtPassword">
52:                             <label class="mdl-textfield__label" for="txtPassword">Enter Password</label>
53:                         </div>
54:                     </div>
55: 
56:                     <div class="mdl-cell mdl-cell--12-col cell_con" style="display:none" id="div_repeat_password" >
57:                         <i class="material-icons margin-left-from-icon">lock</i>
58:                         <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
59:                             <input class="mdl-textfield__input" type="text" id="txtPasswordRepeated">
60:                             <label class="mdl-textfield__label" for="txtPasswordRepeated">Repeat Password</label>
61:                         </div>
62:                     </div>
63: 
64:                     <div class="mdl-cell mdl-cell--12-col login-btn-con">
65:                         <button id="btnAction" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--primary btn">Login</button>
66:                     </div>
67:                 </div>
68: 
69:             </main>
70:         </div>
71: 
72:         <script src="https://www.gstatic.com/firebasejs/4.6.0/firebase.js"></script>
73:         <script src="https://www.gstatic.com/firebasejs/4.6.0/firebase-app.js"></script>
74:         <script src="https://www.gstatic.com/firebasejs/4.6.0/firebase-auth.js"></script>
75: 
76:         <script>
77:             // initialize Firebase
78:             var config = {
79:                 apiKey: "AIzaSyCOa4xX3sPKapwUhStqNIDzuQ4UEFQmr84",
80:                 authDomain: "anothertodoapp.firebaseapp.com",
81:                 databaseURL: "https://anothertodoapp.firebaseio.com",
82:                 projectId: "anothertodoapp",
83:                 storageBucket: "anothertodoapp.appspot.com",
84:                 messagingSenderId: "516966698708"
85:             };
86:             firebase.initializeApp(config);
87:         </script>
88: 
89:         <script src="/index.js"></script>
90:     </body>
91: </html>

Beispiel 3. Einstiegspunkt der Web-Anwendung: Datei index.html.


In Listing 3 machen wir auf die Einbindung von Firebase aufmerksam. Zum einen müssen die Firebase-JavaScript-Bibliotheken auf der WebSeite bekannt sein, siehe dazu die Zeile 72 bis 74. Die Firebase-Informationen zum Projekt selbst – vergleiche die Datei google-services.json des Android-Projekts – sind dieses Mal in der HTML-Datei innerhalb eines script-Tags bekannt zu machen. Sie finden diese Zeilen in den Zeilen 76 bis 87 vor. Für Material Design Lite sind natürlich ebenfalls entsprechende Skript-Dateien einzubinden. Gemäß den MDL-Richtlinien sollten diese Anweisungen gleich am Beginn der HTML-Seite – innerhalb des HEAD-Tags – platziert sein, siehe die Zeilen 7 bis 13.

Die HTML-Anweisungen selbst in Listing 3 bedürfen keiner weiteren Erläuterungen mehr. Der interessante Part findet vielmehr in der Codebehind-Datei – im HTML5-Umfeld eine oder mehrere JavaScript-Dateien – statt. Im konkret vorliegenden Fall wird in Zeile 89 von Listing 3 die JavaScript-Datei index.js in die HTML-Datei inkludiert. Mit ihrer Betrachtung fahren wir in Listing 4 fort:

001: /*jslint browser:true */
002: /*jslint esnext:true*/
003: /*global document, window, firebase */
004: 
005: // retrieve HTML elements
006: var btnAction = document.getElementById('btnAction');
007: var txtEmail = document.getElementById('txtEmail');
008: var txtPassword = document.getElementById('txtPassword');
009: var txtPasswordRepeated = document.getElementById('txtPasswordRepeated');
010: var navLinkSignUp = document.getElementById('navLinkSignUp');
011: var navLinkSignIn = document.getElementById('navLinkSignIn');
012: var paraError = document.getElementById('errorMessage');
013: 
014: var divRepeatPassword = document.getElementById('div_repeat_password');
015: 
016: var state = "SignInMode";  // "SignInMode" or "SignUpMode"
017: 
018: navLinkSignUp.addEventListener('click', function () {
019: 
020:     if (state === "SignInMode") {
021:         state = "SignUpMode";
022:         btnAction.innerText = "Sign Up";
023:         divRepeatPassword.style = 'display:block';
024:         paraError.textContent = "";
025:         clearInput();
026:     }
027: });
028: 
029: navLinkSignIn.addEventListener('click', function () {
030: 
031:     if (state === "SignUpMode") {
032:         state = "SignInMode";
033:         btnAction.innerText = "Sign In";
034:         divRepeatPassword.style = 'display:none';
035:         paraError.textContent = "";
036:         clearInput();
037:     }
038: });
039: 
040: // connect event handler functions
041: btnAction.addEventListener('click', function () {
042: 
043:     'use strict';
044: 
045:     // get email and password
046:     let email = txtEmail.value,
047:         password = txtPassword.value;
048: 
049:     if (state === "SignInMode") {
050: 
051:         if (email !== "" && password !== "") {
052:             signIn(email, password);
053:         } else {
054:             paraError.textContent = "Please enter user credentials !";
055:         }
056:     }
057:     else if (state === "SignUpMode") {
058: 
059:         let passwordRepeated = txtPasswordRepeated.value;
060: 
061:         if (email === "" || password === ""|| passwordRepeated === "") {
062: 
063:             paraError.textContent = "EMail of Password field empty !";
064:             clearInput();
065:             return;
066:         }
067: 
068:         if (password !== passwordRepeated) {
069: 
070:             paraError.textContent = "Passwords are different! Please enter correct credentials!";
071:             clearInput();
072:             return;
073:         }
074: 
075:         signUp(email, password);
076:     }
077: });
078: 
079: // firebase authentication
080: firebase.auth().onAuthStateChanged(function (user) {
081:     'use strict';
082:     if (user) {
083: 
084:         // user is signed in
085:         console.log("I'm logged IN");
086:     } else {
087: 
088:         // no user is signed in
089:         console.log("I'm NOT logged IN");
090:     }
091: });
092: 
093: // helper functions
094: function signIn(email, password) {
095:     'use strict';
096: 
097:     let promise = firebase.auth().signInWithEmailAndPassword(email, password);
098: 
099:     promise
100:         .then(function (user) {
101:         // success
102:         console.log("Email: " + user.email);
103:         window.location.href = "todo.html";
104:     })
105:         .catch(function(error) {
106:         // error handling
107:         paraError.textContent = error.message;
108:     });
109: }
110: 
111: function signUp(email, password) {
112:     'use strict';
113: 
114:     let promise = firebase.auth().createUserWithEmailAndPassword(email, password);
115: 
116:     promise
117:         .then(function (user) {
118:         // success
119:         console.log("Email: " + user.email);
120:         window.location.href = "todo.html";
121:     })
122:         .catch(function(error) {
123:         // error handling
124:         paraError.textContent = error.message;
125:     });
126: }
127: 
128: function clearInput() {
129: 
130:     'use strict';
131:     txtEmail.value = "";
132:     txtPassword.value = "";
133:     txtPasswordRepeated.value = "";
134: }

Beispiel 4. Einstiegspunkt der Web-Anwendung: Datei index.js.


Die beiden JavaScript-Funktionen zum Registrieren oder Anmelden eines Benutzers am App-Projekt finden Sie in den Zeilen 94 bis 104 sowie 111 bis 126 vor. Für eine detaillierte Beschreibung der beiden Methoden createUserWithEmailAndPassword und signInWithEmailAndPassword verweisen wir auf die Firebase-Online-Dokumentation. Natürlich sind wir diesen beiden Funktionen schon einmal begegnet: In der Android-App hatten wir sie auf Basis ihres Java-Bindings aufgerufen (Zeile 71 von Listing 2). Die beiden Funktionen arbeiten in gewohnter Manier asynchron und liefern ein Promise-Objekt zurück. Ein solches Objekt kann sich in einem von vier Zuständen befinden:

  • pending: initialer Status, Operation weder erfolgreich noch gescheitert.

  • settled: Operation weder erfolgreich noch gescheitert, aber nicht mehr initial.

  • fulfilled: Operation erfolgreich.

  • rejected: Operation gescheitert.

Man müsste nicht wie in den Zeilen 97 und 99 von Listing 4 (oder auch 114 und 116) ein Promise-Objekt explizit deklarieren und zum asynchronen Ablauf einsetzen. Das Testen bzw. Debuggen erscheint mir auf diese Weise jedoch einfacher als im Vergleich zur häufig propagierten Fluency, also dem so genannten „functional method chaining“ von Funktionen.

Wurde der Anmeldevorgang (sei es Sign-In oder Sign-Up) erfolgreich abgeschlossen, verzweigt der JavaScript-Code auf eine zweite HTML-Seite. Die entsprechende Anweisung lautet

window.location.href = "todo.html";

Sie finden diese Verzweigung in Listing 4 in den Zeilen 103 und 120 vor. Den Quellcode dieser Seite habe ich auf 3 Dateien todo.html, todo.js und todoitem.js aufgeteilt. Die ersten beiden Dateien beschreiben in klassischer Manier eine Web-Seite mit dazugehörigem JavaScript-Code. In der dritten Datei wird – auf sehr einfache Weise – die Umsetzung eines JavaScript-Objekts mit einer Konstruktor-Funktion demonstriert:

001: <!DOCTYPE html>
002: 
003: <html>
004: 
005:     <head>
006:         <title>Another ToDo Application</title>
007: 
008:         <!-- Material Design Lite -->
009:         <script src="https://code.getmdl.io/1.3.0/material.min.js"></script>
010:         <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.blue_grey-yellow.min.css" />
011: 
012:         <!-- Material Design icon font -->
013:         <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Roboto:400,100,500,300italic,500italic,700italic,900,300">
014:         <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
015: 
016:         <script src="/ToDoItem.js" type="text/javascript"></script>
017: 
018:         <link rel="stylesheet" href="/style.css">
019:     </head>
020: 
021:     <body>
022: 
023:         <div class="mdl-layout mdl-js-layout">
024:             <header class="mdl-layout__header">
025:                 <div class="mdl-layout-icon"></div>
026:                 <div class="mdl-layout__header-row">
027:                     <span class="mdl-layout__title">Another Todo Web App</span>
028:                     <div class="mdl-layout-spacer"></div>
029: 
030:                     <nav class="mdl-navigation">
031:                         <a id="navRefresh" class="mdl-navigation__link" href="#">
032:                             <i class="material-icons">refresh</i>
033:                         </a>
034:                     </nav>
035: 
036:                     <nav class="mdl-navigation">
037:                         <a id="navAdd" class="mdl-navigation__link" href="#">
038:                             <i class="material-icons">add</i>
039:                         </a>
040:                     </nav>
041: 
042:                     <nav class="mdl-navigation">
043:                         <a id="navDel" class="mdl-navigation__link" href="#">
044:                             <i class="material-icons">delete</i>
045:                         </a>
046:                     </nav>
047: 
048:                     <nav class="mdl-navigation">
049:                         <a id="navSignOut" class="mdl-navigation__link" href="#">Sign Out</a>
050:                     </nav>
051: 
052:                 </div>
053:             </header>
054: 
055:             <main class="mdl-layout__content  main_content">
056: 
057:                 <h3>Another ToDo Application</h3>
058: 
059:                 <label>User - EMail Address:</label>
060:                 <div class="mdl-textfield mdl-js-textfield">
061:                     <input class="mdl-textfield__input" type="text" id="txtEmail">
062:                 </div>
063:                 <div class="mdl-layout-spacer"></div>
064: 
065:                 <label>User - Display Name:</label>
066:                 <div class="mdl-textfield mdl-js-textfield">
067:                     <input class="mdl-textfield__input" type="text" id="txtDisplayName">
068:                 </div>
069:                 <div class="mdl-layout-spacer"></div>
070: 
071:                 <label>User Id:</label>
072:                 <div class="mdl-textfield mdl-js-textfield">
073:                     <input class="mdl-textfield__input" type="text" id="txtUid">
074:                 </div>
075:                 <div class="mdl-layout-spacer"></div>
076: 
077:                 <h4>List of Todo's:</h4>
078: 
079:                 <ul id="listTodos" class="mdl-list">
080:                 </ul>
081: 
082:                 <div id="button_delete_toolbar" style="display:none" class="mdl-button-group">
083: 
084:                     <button id="btnDelete" 
085:                             class="button_within_group mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect">
086:                         Delete
087:                     </button>
088:                     <button id="btnAbortDelete" 
089:                             class="button_within_group mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect">
090:                         Abort
091:                     </button>
092: 
093:                 </div>
094: 
095:                 <div id="button_add_toolbar" style="display:none" class="mdl-button-group">
096: 
097:                     <div class="mdl-textfield mdl-js-textfield">
098:                         <input class="mdl-textfield__input" type="text" id="input_todo_item">
099:                         <label class="mdl-textfield__label" for="input_todo_item">ToDo item ...</label>
100:                     </div>
101: 
102:                     <div>
103:                         <button id="btnAdd" 
104:                                 class="button_within_group mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect">
105:                             Add Item
106:                         </button>
107:                         <button id="btnAbortAdd" 
108:                                 class="button_within_group mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect">
109:                             Abort
110:                         </button>
111:                     </div>
112: 
113:                 </div>
114: 
115:             </main>
116:         </div>
117: 
118:         <script src="https://www.gstatic.com/firebasejs/4.6.0/firebase.js"></script>
119:         <script src="https://www.gstatic.com/firebasejs/4.6.0/firebase-app.js"></script>
120:         <script src="https://www.gstatic.com/firebasejs/4.6.0/firebase-auth.js"></script>
121:         <script src="https://www.gstatic.com/firebasejs/4.6.0/firebase-database.js"></script>
122: 
123:         <script>
124:             // initialize Firebase
125:             var config = {
126:                 apiKey: "AIzaSyCOa4xX3sPKapwUhStqNIDzuQ4UEFQmr84",
127:                 authDomain: "anothertodoapp.firebaseapp.com",
128:                 databaseURL: "https://anothertodoapp.firebaseio.com",
129:                 projectId: "anothertodoapp",
130:                 storageBucket: "anothertodoapp.appspot.com",
131:                 messagingSenderId: "516966698708"
132:             };
133:             firebase.initializeApp(config);
134:         </script>
135: 
136:         <!--  <script src="https://code.jquery.com/jquery-3.1.0.js"></script> -->
137:         <script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
138:         <script src="/todo.js"></script>
139: 
140:     </body>
141: 
142: </html>

Beispiel 5. Auflistung aller Notizen eines Benutzers: HTML.


001: /*jshint strict:true */
002: /*jshint unused:false */
003: 
004: /*jslint browser:true */
005: /*jslint esnext:true*/
006: /*global document, firebase */
007: /*global componentHandler */
008: /*global ToDoItem */
009: /*jslint plusplus: true */
010: 
011: // custom event - event based update of page
012: const FirebaseDataEvent = "onfirebasedatainvalid";
013: const Event_RequestToDoList = "onRequestToDoList";
014: const Event_DisplayToDoListNormal = "onDisplayToDoListNormal";
015: 
016: // HTML elements - navigation bar
017: var navRefreshItem = document.getElementById('navRefresh');
018: var navAddItem = document.getElementById('navAdd');
019: var navDeleteItem = document.getElementById('navDel');
020: var navSignOut = document.getElementById('navSignOut');
021: 
022: // HTML elements - bottom toolbar to add an item
023: var toolbarAdd = document.getElementById('button_add_toolbar');
024: var btnAdd = document.getElementById('btnAdd');
025: var btnAbortAdd = document.getElementById('btnAbortAdd');
026: var inputToDoItem = document.getElementById('input_todo_item');
027: 
028: // HTML elements - bottom toolbar to delete item(s)
029: var toolbarDelete = document.getElementById('button_delete_toolbar');
030: var btnDelete = document.getElementById('btnDelete');
031: var btnAbortDelete = document.getElementById('btnAbortDelete');
032: 
033: // HTML elements - user information
034: var txtDisplayName = document.getElementById('txtDisplayName');
035: var txtEmail = document.getElementById('txtEmail');
036: var txtEmailVerified = document.getElementById('txtEmailVerified');
037: var txtUid = document.getElementById('txtUid');
038: 
039: // HTML elements - ToDo list
040: var listTodos = document.getElementById('listTodos');
041: 
042: // local state of current 'todo' items list
043: var todoListItems = [];
044: 
045: // get a reference to the database service
046: var db = firebase.database();
047: var uidCurrentUser = "";
048: 
049: // ==================================================================================
050: // firebase authentication
051: 
052: firebase.auth().onAuthStateChanged(function (user) {
053: 
054:     'use strict';
055:     if (user !== null) {
056: 
057:         console.log("User " + user.email + " logged IN");
058:         uidCurrentUser = user.uid;
059:         initComponents (user);
060: 
061:         let event = new CustomEvent(FirebaseDataEvent, { detail: Event_RequestToDoList });
062:         window.dispatchEvent(event);
063: 
064:     } else {
065: 
066:         console.log("user logged OUT");
067:         uidCurrentUser = '';
068:     }
069: });
070: 
071: // signout event handler
072: navSignOut.addEventListener('click', function () {
073: 
074:     'use strict';
075:     console.log('navSignOut clicked');
076: 
077:     firebase.auth().signOut().then(function () {
078: 
079:         // sign-out successful ...
080:         console.log('User successfully signed OUT !');
081:         window.location.href = 'index.html';
082: 
083:     }, function (error) {
084:         console.log('User NOT signed out !' + error.message);
085:     });
086: });
087: 
088: // ==================================================================================
089: // miscellaneous event handler methods: page load and custom events
090: 
091: window.onload = function(){
092: 
093:     'use strict';
094:     console.log('Todo Page LOADED');
095: 
096:     window.addEventListener (FirebaseDataEvent, handleEvent, false);
097: };
098: 
099: function handleEvent(event)
100: {
101:     'use strict';
102:     console.log('handleEvent [' + event.detail + ']');
103: 
104:     if (event.detail === Event_RequestToDoList) {
105: 
106:         if (uidCurrentUser !== "") {
107:             readToDoList(uidCurrentUser, Event_DisplayToDoListNormal);
108:         }
109:     }
110:     else if (event.detail === Event_DisplayToDoListNormal) {
111: 
112:         showToDoListNormal (todoListItems);
113:     }
114: }
115: 
116: // ==================================================================================
117: // top navigation bar event handler
118: 
119: // refresh item list
120: navRefreshItem.addEventListener('click', function () {
121: 
122:     'use strict';
123:     console.log('"Refresh Item" button clicked');
124: 
125:     let event = new CustomEvent(FirebaseDataEvent, { detail: Event_RequestToDoList });
126:     window.dispatchEvent(event);
127: });
128: 
129: // add item - tbd
130: navAddItem.addEventListener('click', function () {
131: 
132:     'use strict';
133:     console.log('"Add Item" button clicked');
134: 
135:     // make 'sub menu' visible
136:     toolbarAdd.style = 'display:block';
137:     navRefreshItem.style = 'display:none';
138:     navAddItem.style = 'display:none';
139:     navDeleteItem.style = 'display:none';
140: });
141: 
142: // delete item(s)
143: navDeleteItem.addEventListener('click', function () {
144: 
145:     'use strict';
146:     console.log('"Delete Item" button clicked');
147: 
148:     // make 'sub menu' visible
149:     toolbarDelete.style = 'display:block';
150:     navRefreshItem.style = 'display:none';
151:     navAddItem.style = 'display:none';
152:     navDeleteItem.style = 'display:none';
153: 
154:     showToDoListWithCheckboxes (todoListItems);
155: });
156: 
157: // ==================================================================================
158: // bottom bar (footer) event handler (add item - tbd)
159: 
160: btnAdd.addEventListener('click', function () {
161: 
162:     'use strict';
163:     console.log('Add button clicked: ' + todoListItems.length);
164: 
165:     // read input from text box
166:     var item = inputToDoItem.value;
167:     console.log ("TEXT: " + item);
168: 
169:     // poke item to firebase data storage
170:     addToDoItem (item);
171: 
172:     // make menu invisible
173:     toolbarAdd.style = 'display:none';
174:     navRefreshItem.style = 'display:block';
175:     navAddItem.style = 'display:block';
176:     navDeleteItem.style = 'display:block';
177: 
178:     let event = new CustomEvent(FirebaseDataEvent, { detail: Event_RequestToDoList });
179:     window.dispatchEvent(event);
180: });
181: 
182: function addToDoItem(item) {
183: 
184:     'use strict';
185: 
186:     // build firebase reference string
187:     let refstring = 'users/' + uidCurrentUser + '/items';
188:     let newItemRef = db.ref(refstring).push();
189:     newItemRef.set ({ title: item }, function (error) {
190:         if (error) {
191:             console.log("Data could not be saved: " + error);
192: 
193:         } else {
194:             console.log("Data saved successfully.");
195:         }
196:     });
197: }
198: 
199: btnAbortAdd.addEventListener('click', function () {
200: 
201:     'use strict';
202:     console.log('Abort button clicked');
203: 
204:     // make 'sub menu' invisible
205:     toolbarAdd.style = 'display:none';
206:     navRefreshItem.style = 'display:block';
207:     navAddItem.style = 'display:block';
208:     navDeleteItem.style = 'display:block';
209: 
210:     let event = new CustomEvent(FirebaseDataEvent, { detail: Event_DisplayToDoListNormal });
211:     window.dispatchEvent(event);
212: });
213: 
214: // ==================================================================================
215: // footer bar event handler (delete items)
216: 
217: btnDelete.addEventListener('click', function () {
218: 
219:     'use strict';
220:     console.log('Delete button clicked: ' + todoListItems.length);
221: 
222:     var numItemsToDelete = 0;
223:     for (let i = 0; i < todoListItems.length; i++) {
224: 
225:         var checkBox = todoListItems[i].node;
226:         if (checkBox.checked === true) {
227: 
228:             numItemsToDelete ++;
229:             deleteToDoItem(i);
230:         }
231:     }
232: 
233:     // make 'sub menu' invisible
234:     toolbarDelete.style = 'display:none';
235:     navRefreshItem.style = 'display:block';
236:     navAddItem.style = 'display:block';
237:     navDeleteItem.style = 'display:block';
238: 
239:     if (numItemsToDelete > 0) {
240: 
241:         // request current list from server
242:         let event = new CustomEvent(FirebaseDataEvent, { detail: Event_RequestToDoList });
243:         window.dispatchEvent(event);
244:     }
245:     else {
246: 
247:         // display last read list from client - no server round-trip necessary
248:         let event = new CustomEvent(FirebaseDataEvent, { detail: Event_DisplayToDoListNormal });
249:         window.dispatchEvent(event);
250:     }
251: });
252: 
253: function deleteToDoItem(index) {
254: 
255:     'use strict';
256: 
257:     // build firebase reference string
258:     let key = todoListItems[index].key;
259:     let refstring = 'users/' + uidCurrentUser + '/items/' + key;
260:     let ref = db.ref(refstring);
261: 
262:     ref.remove();
263: }
264: 
265: btnAbortDelete.addEventListener('click', function () {
266: 
267:     'use strict';
268:     console.log('Abort button clicked');
269: 
270:     // make 'sub menu' invisible
271:     toolbarDelete.style = 'display:none';
272:     navRefreshItem.style = 'display:block';
273:     navAddItem.style = 'display:block';
274:     navDeleteItem.style = 'display:block';
275: 
276:     let event = new CustomEvent(FirebaseDataEvent, { detail: Event_DisplayToDoListNormal });
277:     window.dispatchEvent(event);
278: });
279: 
280: // ==================================================================================
281: // private helper functions
282: 
283: function initComponents(user) {
284: 
285:     'use strict';
286: 
287:     let name = user.displayName,
288:         email = user.email,
289:         photoUrl = user.photoURL,
290:         emailVerified = user.emailVerified,
291:         uid = user.uid;
292: 
293:     if (name !== null) {
294:         txtDisplayName.value = uid;
295:     } else {
296:         txtDisplayName.value = '<no display found>';
297:     }
298: 
299:     txtEmail.value = email;
300:     txtUid.value = uid;
301: }
302: 
303: function readToDoList(uid, kind) {
304: 
305:     'use strict';
306:     todoListItems = [];
307: 
308:     let refstring = 'users/' + uid + '/items/';
309:     db.ref(refstring).once('value').then(function (snapshot) {
310: 
311:         snapshot.forEach(function (childSnapshot) {
312: 
313:             let key = childSnapshot.key,
314:                 data = childSnapshot.val(),
315:                 title = data.title;
316: 
317:             // enter todo item into list
318:             let item = new ToDoItem (key, title);
319:             todoListItems.push(item);
320:         });
321: 
322:         // list of ToDo items updated, fire 'display list' event
323:         let event = new CustomEvent(FirebaseDataEvent, { detail: kind });
324:         window.dispatchEvent(event);
325:     });
326: }
327: 
328: function showToDoListNormal(list) {
329: 
330:     'use strict';
331: 
332:     // clear list
333:     listTodos.innerHTML = '';
334: 
335:     for (let i = 0; i < list.length; i++) {
336: 
337:         // build list item string
338:         var linePrefix = (i < 10) ? '  ' : (i < 100) ? ' ' : '',
339:             line = linePrefix + (i+1) + ': ' + list[i].title;
340: 
341:         // add a 'material design lite' node to a list dynamically
342:         let node = document.createElement('li');         // create a <li> node
343:         node.setAttribute('class', 'mdl-list__item');    // set an attribute
344: 
345:         let span = document.createElement('span');       // create a <span> node
346:         span.setAttribute('class', 'todo_margin mdl-list__item-primary-content');
347: 
348:         let textnode = document.createTextNode(line);    // create a text node
349:         node.appendChild(span);                          // append <span> to <li>
350:         span.appendChild(textnode);                      // append text to <span>
351:         listTodos.appendChild(node);                     // append <li> to <ul>
352:     }
353: 
354:     componentHandler.upgradeDom();
355: }
356: 
357: function showToDoListWithCheckboxes(items) {
358: 
359:     'use strict';
360: 
361:     // clear list
362:     listTodos.innerHTML = '';
363: 
364:     for (let i = 0; i < items.length; i++) {
365: 
366:         // add a 'material design lite' node to a list dynamically
367:         let liNode = document.createElement('li');
368:         liNode.setAttribute('class', 'mdl-list__item');
369: 
370:         let labelNode = document.createElement('label');    // create a <label> node
371:         labelNode.setAttribute('class', 'mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect');
372:         labelNode.setAttribute('for', 'checkbox' + i);
373: 
374:         let inputNode = document.createElement('input');
375:         inputNode.setAttribute('type', 'checkbox');
376:         inputNode.setAttribute('id', 'checkbox' + i);
377:         inputNode.setAttribute('class', 'mdl-checkbox__input');
378: 
379:         // todoListCheckboxes.push (inputNode);
380:         items[i].node = inputNode;
381: 
382:         let spanNode = document.createElement('span');
383:         spanNode.setAttribute('class', 'todo_margin mdl-checkbox__label');
384: 
385:         let textnode = document.createTextNode(items[i].title);
386: 
387:         spanNode.appendChild(textnode);
388:         labelNode.appendChild(inputNode);
389:         labelNode.appendChild(spanNode);
390:         liNode.appendChild(labelNode);
391: 
392:         listTodos.appendChild(liNode);
393:     }
394: 
395:     componentHandler.upgradeDom('MaterialCheckbox');
396: }

Beispiel 6. Auflistung aller Notizen eines Benutzers: JavaScript-Code.


01: /*global window */
02: /*jshint unused:false*/
03: 
04: function ToDoItem(key, title) {
05:     'use strict';
06: 
07:     // key and todo text from firebase data storage
08:     this.key = key;
09:     this.title = title;
10: 
11:     // checkbox not yet known
12:     this.node = null;
13:     this.toBeDeleted = false;
14: 
15:     this.print = function () {
16:         window.console.log("Title: " + this.title);
17:     };
18: }

Beispiel 7. Konstruktor-Funktion für ein JavaScript-ToDoItem-Objekt.