Online Eiscreme Bestellungen mit Dialogflow und Firebase —
Teil 3: Android App

1. Aufgabe

In den ersten beiden Teilen dieser Fallstudie haben wir eine Eisbestellung via Google Sprachassistent an das Firebase Backend weitergereicht. Die Logik des Bestellvorgangs wird durch Firebase Functions abgearbeitet, die Daten selbst werden Bestellung für Bestellung in der Firebase Realtime Database abgelegt. Erstellen Sie eine Android App, die alle vorliegenden Bestellungen anzeigt. Ferner soll es möglich sein, eine bestimmte Bestellung in der Eisdiele abzuholen, also „auszuchecken“. Der dazugehörige Datensatz der Eisbestellung ist dabei in der Firebase Realtime Database zu löschen.

Hinweis: Die nachfolgend vorgestellte Lösung ist nicht als App für einen Einsatz im Produktivbetrieb gedacht, sondern vielmehr als ein „Proof of Concept“. Zum einen verzichten wir aus Aufwandsgründen auf Registrierungs- bzw. Login-Funktionalitäten („Sign In“, „Sign Up“). Zum anderen werden Sie feststellen, dass in der Übersichtsdarstellung (MainActivity) die Bestellungen aller Kunden zu sehen sind. Die App legt folglich den Blick auf alle vorhandenen Bestellungen offen, der Zugriff der App nur auf die Daten eines einzelnen Benutzers ist weder durch eine Authentifizierung verriegelt noch im weiteren Ablauf der App durch andere Mechanismen implementiert. Das ergibt zunächst einmal keinen Sinn, denn man möchte in einer client-spezifischen App nur die Bestellungen eines einzelnen Kunden sehen können, aber nicht die von allen.

Der simple Grund hierfür liegt darin, dass es mir bei vorgestellten App einfach nur darum ging, möglichst aufwandsarm den kompletten Handlungsstrang – beginnend mit der Erkennung eines Datensatzes (Bestellung) durch das Dialogflow-Spracherkennungsportal (Eingabe) über Firebase Cloud Functions (Verarbeitung) bis hin zu seiner finalen Bearbeitung (Ausgabe) – in einer Android-App nachzustellen. Noch fehlende Funktionalitäten, die eine echte Produkt-App auszeichnen würden, sollten aber ohne größere Probleme nachrüstbar sein.

2. Lösung

Quellcode: Siehe auch github.com/peterloos/Android_OnlineIcecreamParlor.git.

Die im nachfolgenden vorgestellte App besitzt zwei Activities, eine MainActivity und eine DetailsActivity. In der MainActivity wird ein Überblick über alle vorliegenden Bestellungen gegeben, die von der Eisdiele noch zu produzieren sind (Abbildung 1). Details zu einer bestimmten Bestellung lassen sich in der DetailsActivity näher betrachten. In dieser zweiten Activity ist es dann auch möglich, eine bestimmte Bestellung „auszuchecken“.

Übersicht aller Online-Bestellung in der Eisdiele.

Abbildung 1. Übersicht aller Online-Bestellung in der Eisdiele.


Die Details einer Eisbestellung – dazu zählt vor allem die eindeutige Bestellnummer – lassen sich durch einen Klick auf die Zeile in der Übersichtsdarstellung in einer separaten Detailansicht anzeigen (Abbildung 2):

Detailansicht zu einer Online-Eisbestellung.

Abbildung 2. Detailansicht zu einer Online-Eisbestellung.


Die Gestaltung der MainActivity ist in ihrem prinzipiellen Aufbau einfach ausgelegt. Sowohl das User Interface – statisch in einem XML-Dokument abgelegt – wie auch der Logik-Anteil fallen knapp aus:

01: <?xml version="1.0" encoding="utf-8"?>
02: 
03: <android.support.constraint.ConstraintLayout
04:     xmlns:android="http://schemas.android.com/apk/res/android"
05:     xmlns:app="http://schemas.android.com/apk/res-auto"
06:     xmlns:tools="http://schemas.android.com/tools"
07:     android:layout_width="match_parent"
08:     android:layout_height="match_parent"
09:     tools:context=".activities.MainActivity"
10:     tools:layout_editor_absoluteY="81dp">
11: 
12:     <android.support.v7.widget.RecyclerView
13:         android:id="@+id/recyclerViewPickupNames"
14:         android:layout_width="0dp"
15:         android:layout_height="0dp"
16:         android:layout_marginStart="8dp"
17:         android:layout_marginTop="8dp"
18:         android:layout_marginEnd="8dp"
19:         android:layout_marginBottom="8dp"
20:         app:layout_constraintBottom_toBottomOf="parent"
21:         app:layout_constraintEnd_toEndOf="parent"
22:         app:layout_constraintStart_toStartOf="parent"
23:         app:layout_constraintTop_toBottomOf="@+id/textView"
24:         app:layout_constraintVertical_bias="0.0" />
25: 
26:     <TextView
27:         android:id="@+id/textView"
28:         android:layout_width="0dp"
29:         android:layout_height="wrap_content"
30:         android:layout_marginStart="8dp"
31:         android:layout_marginTop="8dp"
32:         android:layout_marginEnd="8dp"
33:         android:text="@string/list_of_available_orders"
34:         android:textSize="28sp"
35:         android:textAlignment="center"
36:         app:layout_constraintEnd_toEndOf="parent"
37:         app:layout_constraintStart_toStartOf="parent"
38:         app:layout_constraintTop_toTopOf="parent" />
39: 
40: </android.support.constraint.ConstraintLayout>

Beispiel 1. Klasse MainActivity: User Interface (XML).


01: package de.peterloos.petersicecreamparlor.activities;
02: 
03: import android.content.Intent;
04: import android.support.v7.app.AppCompatActivity;
05: import android.os.Bundle;
06: import android.support.v7.widget.DividerItemDecoration;
07: import android.support.v7.widget.LinearLayoutManager;
08: import android.support.v7.widget.RecyclerView;
09: import android.util.Log;
10: import android.view.View;
11: 
12: import de.peterloos.petersicecreamparlor.Globals;
13: import de.peterloos.petersicecreamparlor.interfaces.MyItemClickListener;
14: import de.peterloos.petersicecreamparlor.R;
15: import de.peterloos.petersicecreamparlor.adapters.RecyclerViewAdapter;
16: import de.peterloos.petersicecreamparlor.models.OrderModel;
17: import de.peterloos.petersicecreamparlor.parcels.OrderParcel;
18: 
19: public class MainActivity extends AppCompatActivity implements MyItemClickListener {
20: 
21:     private RecyclerView recyclerView;
22:     private RecyclerViewAdapter adapter;
23: 
24:     @Override
25:     protected void onCreate(Bundle savedInstanceState) {
26:         super.onCreate(savedInstanceState);
27:         this.setContentView(R.layout.activity_main);
28: 
29:         // set up recycler view for pickup names
30:         this.recyclerView = this.findViewById(R.id.recyclerViewPickupNames);
31:         LinearLayoutManager layoutManager = new LinearLayoutManager(this);
32:         this.recyclerView.setLayoutManager(layoutManager);
33: 
34:         DividerItemDecoration dividerItemDecoration =
35:                 new DividerItemDecoration(
36:                         this.recyclerView.getContext(),
37:                         layoutManager.getOrientation()
38:                 );
39:         this.recyclerView.addItemDecoration(dividerItemDecoration);
40:     }
41: 
42:     @Override
43:     public void onStart() {
44:         super.onStart();
45: 
46:         // listen for orders
47:         this.adapter = new RecyclerViewAdapter(this);
48:         this.adapter.setClickListener(this);
49:         this.recyclerView.setAdapter(this.adapter);
50:     }
51: 
52:     @Override
53:     public void onStop() {
54:         super.onStop();
55: 
56:         // clean up orders listener
57:         this.adapter.cleanupListener();
58:     }
59: 
60:     @Override
61:     public void onItemClick(View view, int position) {
62: 
63:         OrderModel order = this.adapter.getOrder(position);
64:         Intent intent = new Intent(this.getApplicationContext(), DetailsActivity.class);
65:         OrderParcel parcel = new OrderParcel(
66:                 order.getKey(),
67:                 order.getTimeOfOrder(),
68:                 order.getPickupName(),
69:                 order.getScoops(),
70:                 order.getFlavorsArray(),
71:                 order.getContainer()
72:         );
73:         intent.putExtra(Globals.ORDER_PARCEL, parcel);
74:         this.startActivity(intent);
75:     }
76: }

Beispiel 2. Klasse MainActivity: Logik-Anteil (Java).


Die Aufbereitung der Daten für das RecyclerView-Steuerelement erfolgt in einem separaten Adapter-Objekt. Hier kommt nun das Firebase-Backend ins Spiel: Sowohl für das erstmalige Anzeigen des kompletten Bestelleingangs wie auch bei Änderungen (neue Bestellung hinzufügen, Bestellung abholen und damit im Bestand löschen) sind entsprechende Vorkehrungen zu leisten, siehe Listing 3:

001: package de.peterloos.petersicecreamparlor.adapters;
002: 
003: import android.content.Context;
004: import android.support.annotation.NonNull;
005: import android.support.annotation.Nullable;
006: import android.support.v7.widget.RecyclerView;
007: import android.util.Log;
008: import android.view.LayoutInflater;
009: import android.view.View;
010: import android.view.ViewGroup;
011: import android.widget.TextView;
012: 
013: import com.google.firebase.database.ChildEventListener;
014: import com.google.firebase.database.DataSnapshot;
015: import com.google.firebase.database.DatabaseError;
016: import com.google.firebase.database.DatabaseReference;
017: import com.google.firebase.database.FirebaseDatabase;
018: 
019: import java.util.ArrayList;
020: import java.util.List;
021: import java.util.Locale;
022: 
023: import de.peterloos.petersicecreamparlor.Globals;
024: import de.peterloos.petersicecreamparlor.interfaces.MyItemClickListener;
025: import de.peterloos.petersicecreamparlor.models.OrderModel;
026: import de.peterloos.petersicecreamparlor.R;
027: 
028: public class RecyclerViewAdapter extends RecyclerView.Adapter<ViewHolder> {
029: 
030:     private Context context;
031:     private LayoutInflater inflater;
032: 
033:     private List<OrderModel> orderModels;
034:     private List<String> keys;
035: 
036:     private DatabaseReference ordersRef;
037:     private ChildEventListener childEventListener;
038: 
039:     private MyItemClickListener itemClickListener;
040: 
041:     public RecyclerViewAdapter(final Context context) {
042: 
043:         this.context = context;
044:         this.inflater = LayoutInflater.from(this.context);
045: 
046:         this.orderModels = new ArrayList<>();
047:         this.keys = new ArrayList<>();
048: 
049:         // initialize database
050:         this.ordersRef = FirebaseDatabase.getInstance().getReference().child("orders");
051: 
052:         // create firebase child event listener
053:         this.childEventListener = new IceParlorChildEventListener();
054:         this.ordersRef.addChildEventListener(this.childEventListener);
055:     }
056: 
057:     @NonNull
058:     @Override
059:     public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int i) {
060: 
061:         // inflate row layout from xml when needed
062:         View view = inflater.inflate(R.layout.recyclerview_row, parent, false);
063:         ViewHolder holder = new ViewHolder(view);
064:         holder.setClickListener(this.itemClickListener);
065:         return holder;
066:     }
067: 
068:     @Override
069:     public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
070: 
071:         OrderModel order = this.orderModels.get(position);
072: 
073:         String sId = this.context.getResources().getString(R.string.pickup_id);
074:         String sRow = String.format(Locale.getDefault(), "%s: %s",
075:                 sId, Long.toString(order.getPickupName()));
076:         viewHolder.getTextView().setText(sRow);
077:     }
078: 
079:     @Override
080:     public int getItemCount() {
081:         return this.orderModels.size();
082:     }
083: 
084:     // convenience methods for getting data at click position
085:     public OrderModel getOrder(int id) {
086:         return this.orderModels.get(id);
087:     }
088: 
089:     // allows clicks events to be caught
090:     public void setClickListener(MyItemClickListener itemClickListener) {
091:         this.itemClickListener = itemClickListener;
092:     }
093: 
094:     public void cleanupListener() {
095:         if (this.itemClickListener != null) {
096:             this.itemClickListener = null;
097:         }
098: 
099:         if (this.childEventListener != null) {
100:             this.ordersRef.removeEventListener(this.childEventListener);
101:         }
102:     }
103: 
104:     private class IceParlorChildEventListener implements ChildEventListener {
105: 
106:         @Override
107:         public void onChildAdded(
108:                 @NonNull DataSnapshot dataSnapshot,
109:                 @Nullable String s) {
110: 
111:             // a new order has been added, add it to the displayed list
112:             OrderModel order = dataSnapshot.getValue(OrderModel.class);
113:             String key = dataSnapshot.getKey();
114: 
115:             // patch order object with firebase key
116:             order.setKey(key);
117: 
118:             // insert new order
119:             RecyclerViewAdapter.this.orderModels.add(order);
120:             RecyclerViewAdapter.this.keys.add(key);
121:             RecyclerViewAdapter.this.notifyItemInserted(
122:                     RecyclerViewAdapter.this.orderModels.size() - 1
123:             );
124: 
125:             Log.v(Globals.TAG, "onChildAdded: " + order.toString());
126:         }
127: 
128:         @Override
129:         public void onChildChanged(
130:                 @NonNull DataSnapshot dataSnapshot,
131:                 @Nullable String s) {
132: 
133:             Log.v(Globals.TAG, "onChildChanged");
134:         }
135: 
136:         @Override
137:         public void onChildRemoved(@NonNull DataSnapshot dataSnapshot) {
138: 
139:             String logMsg = "onChildRemoved";
140:             Log.v(Globals.TAG, logMsg);
141: 
142:             // a pickup name has been redeemed, use the key to determine
143:             // if we are displaying this pickup name and if so remove it
144:             String key = dataSnapshot.getKey();
145: 
146:             int index = RecyclerViewAdapter.this.keys.indexOf(key);
147:             if (index > -1) {
148:                 // Remove data from the list
149:                 RecyclerViewAdapter.this.orderModels.remove(index);
150:                 RecyclerViewAdapter.this.keys.remove(index);
151: 
152:                 // update the recycler view
153:                 RecyclerViewAdapter.this.notifyItemRemoved(index);
154:                 logMsg = String.format(
155:                         Locale.getDefault(),
156:                         "onChildRemoved: removed pickname at %s",
157:                         Integer.toString(index));
158:             } else {
159:                 logMsg = String.format(
160:                         Locale.getDefault(),
161:                         "Internal Error: onChildRemoved: unknown_child = %s",
162:                         key);
163:             }
164: 
165:             Log.v(Globals.TAG, logMsg);
166:         }
167: 
168:         @Override
169:         public void onChildMoved(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {
170: 
171:             Log.v(Globals.TAG, "onChildMoved");
172:         }
173: 
174:         @Override
175:         public void onCancelled(@NonNull DatabaseError databaseError) {
176: 
177:             Log.v(Globals.TAG, "onCancelled");
178:         }
179:     }
180: }
181: 
182: class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
183: 
184:     private TextView textView;
185:     private MyItemClickListener clickListener;
186: 
187:     public ViewHolder(View itemView) {
188:         super(itemView);
189: 
190:         this.textView = itemView.findViewById(R.id.tvPickupName);
191:         itemView.setOnClickListener(this);
192:     }
193: 
194:     @Override
195:     public void onClick(View view) {
196: 
197:         if (this.clickListener != null) {
198:             this.clickListener.onItemClick(view, this.getAdapterPosition());
199:         }
200:     }
201: 
202:     public TextView getTextView() {
203:         return this.textView;
204:     }
205: 
206:     public void setClickListener(MyItemClickListener clickListener) {
207:         this.clickListener = clickListener;
208:     }
209: }

Beispiel 3. Klasse RecyclerViewAdapter: Realisierung einer Adapter-Klasse für das RecyclerView-Steuerelement.


Die RecyclerViewAdapter-Klasse stellt mehr oder minder das Kernstück dieser App das. Im Sinne einer geschickten Strukturierung der gesamten App sind alle lesenden und schreibenden Zugriffe auf das Firebase-Backend innerhalb dieser Klasse gekapselt. Um Änderungen im Datenbestand zeitnah in der App-Oberfläche anzeigen zu können, besitzt die Klasse eine Implementierung der ChildEventListener-Schnittstelle. Damit werden alle Änderungen unterhalb eines bestimmten Firebase-Knotens erfasst, in unserem Fall bietet sich der Pfad /orders/ an. Mit Hilfe der vier Methoden onChildAdded, onChildChanged, onChildRemoved und onChildMoved werden die Änderungen im Datenbestand beobachtet, in unserer App sind vor allem die beiden Methoden onChildAdded und onChildRemoved wichtig, da sie pro neuer Bestellung oder zu löschender Bestellung aufgerufen werden.

Beim Auslesen der Daten aus der Firebase Realtime Database benötigt man Utility-Java-Klassen, die Informationen im JSON-Format automatisiert auf Java-Objekte umsetzen. Ein Firebase-Datasnapshot in JSON, zum Beispiel der Gestalt

{ 
    key = -LPtr43aTK64y9OTslRg,
    value = {
        pickupName = 3, 
        timeOfOrder = 1540716515681, 
        scoops = 2, 
        container = cone,
        flavors = { 0 = strawberry, 1 = chocolate }
    }
}

ist zunächst in gültiges JSON umzuwandeln:

{
    "key": "-LPtr43aTK64y9OTslRg",
    "value": {
      "pickupName": 3,
      "timeOfOrder": 1540716515681,
      "scoops": 2,
      "container": "cone",
      "flavors": [
        "vanilla",
        "strawberry"
      ]
    }
}

Keine Angst, dass fällt nicht in unseren Aufgabenbereich. Wir benötigen allerdings eine Java-Klasse, die eine derartige JSON-Zeichenkette auf ein Java-Objekt abbilden kann, oder wie man im Fachjargon auch sagt: deserialisiert. Da wir es mit einer „Bestellung“ zu tun haben, habe ich die Klasse OrderModel getauft. Ihre Realisierung finden Sie in Listing 4 vor:

001: package de.peterloos.petersicecreamparlor.models;
002: 
003: import android.support.annotation.NonNull;
004: 
005: import com.google.firebase.database.IgnoreExtraProperties;
006: 
007: import java.util.ArrayList;
008: import java.util.List;
009: import java.util.Locale;
010: 
011: @IgnoreExtraProperties
012: public class OrderModel {
013: 
014:     private String key;
015:     private long timeOfOrder;
016:     private String container;
017:     private List<String> flavors;
018:     private long pickupName;
019:     private int scoops;
020: 
021:     // mandatory: default constructor that takes no arguments
022:     public OrderModel() {
023:         this.key = "";
024:         this.timeOfOrder = 0;
025:         this.container = "";
026:         this.flavors = new ArrayList<>();
027:         this.pickupName = 0;
028:         this.scoops = 0;
029:     }
030: 
031:     @Override
032:     @NonNull
033:     public String toString() {
034:         return this.print();
035:     }
036: 
037:     @SuppressWarnings("unused")
038:     public long getTimeOfOrder() {
039:         return this.timeOfOrder;
040:     }
041: 
042:     @SuppressWarnings("unused")
043:     public void setTimeOfOrder(long timeOfOrder) {
044:         this.timeOfOrder = timeOfOrder;
045:     }
046: 
047:     @SuppressWarnings("unused")
048:     public String getKey() {
049:         return this.key;
050:     }
051: 
052:     @SuppressWarnings("unused")
053:     public void setKey(String key) {
054:         this.key = key;
055:     }
056: 
057:     @SuppressWarnings("unused")
058:     public String getContainer() {
059:         return this.container;
060:     }
061: 
062:     @SuppressWarnings("unused")
063:     public void setContainer(String container) {
064:         this.container = container;
065:     }
066: 
067:     @SuppressWarnings("unused")
068:     public List<String> getFlavors() {
069:         return this.flavors;
070:     }
071: 
072:     @SuppressWarnings("unused")
073:     public String[] getFlavorsArray() {
074:         String tmp[] = new String[this.flavors.size()];
075:         return this.flavors.toArray(tmp);
076:     }
077: 
078:     @SuppressWarnings("unused")
079:     public void setFlavors(List<String> flavors) {
080:         this.flavors = flavors;
081:     }
082: 
083:     public long getPickupName() {
084:         return this.pickupName;
085:     }
086: 
087:     @SuppressWarnings("unused")
088:     public void setPickupName(long pickupName) {
089:         this.pickupName = pickupName;
090:     }
091: 
092:     @SuppressWarnings("unused")
093:     public int getScoops() {
094:         return this.scoops;
095:     }
096: 
097:     @SuppressWarnings("unused")
098:     public void setScoops(int scoops) {
099:         this.scoops = scoops;
100:     }
101: 
102:     private String print() {
103:         StringBuilder sb = new StringBuilder();
104:         String s =
105:                 String.format(Locale.getDefault(),
106:                         "Key: %s [TimeStamp: %d]", this.key, this.timeOfOrder);
107:         sb.append(s);
108:         sb.append(" - ");
109:         sb.append(String.format(Locale.getDefault(), "PickupName: %d", this.pickupName));
110:         sb.append(" - ");
111:         sb.append(String.format(Locale.getDefault(), "Num Scoops: %d", this.scoops));
112:         sb.append(" - ");
113:         sb.append(String.format("Container:  %s", this.container));
114:         sb.append(" - ");
115: 
116:         if (this.flavors == null) {
117:             sb.append("flavors == null ");
118:             sb.append(" - ");
119:         } else {
120: 
121:             sb.append("Flavors: ");
122:             sb.append(" - ");
123: 
124:             for (int i = 0; i < this.flavors.size(); i++) {
125:                 String flav = String.format(Locale.getDefault(), "   %d: %s",
126:                         (i + 1), this.flavors.get(i));
127:                 sb.append(flav);
128:                 sb.append(System.getProperty("line.separator"));
129:             }
130:         }
131: 
132:         return sb.toString();
133:     }
134: }

Beispiel 4. Klasse OrderModel: Deserialisierung von Firebase-Objekten.


Bitte beachten Sie: Den Hauptverwendungsweck dieser Klasse finden Sie in einer einzigen Zeile 112 von Listing 3 vor:

OrderModel order = dataSnapshot.getValue(OrderModel.class);

Die Methode getValue kann man als Deserialisierer bezeichnen – ihr einziges Argument ist die Definition einer Klasse, um damit den Umwandlungsprozess JSON ? Java-Objekt vornehmen zu können.

Um von einer bestimmten Bestellung in der Übersichtsdarstellung zu den Details dieser Bestellung zu gelangen, lassen sich einzelne Zeilen des RecyclerView-Steuerelement via Touch-Event selektieren. Die Details hierzu entnehmen Sie bitte Listing 3. Damit haben wir eine Überleitung zur zweiten Activity dieser App hergestellt, ihr Klassenname lautet DetailsActivity. Wie der Name vermuten lässt, kümmert sie sich um die visuelle Darstellung der Details einer einzelnen Bestellung. Das Grundgerüst der Klasse DetailsActivity können Sie Listing 5 und Listing 6 entnehmen:

01: <?xml version="1.0" encoding="utf-8"?>
02: 
03: <LinearLayout
04: 
05:     xmlns:android="http://schemas.android.com/apk/res/android"
06:     xmlns:tools="http://schemas.android.com/tools"
07:     android:layout_width="match_parent"
08:     android:layout_height="match_parent"
09:     android:orientation="vertical"
10:     tools:context=".activities.DetailsActivity">
11: 
12:     <TextView
13:         android:id="@+id/textViewHeader"
14:         android:layout_width="match_parent"
15:         android:layout_height="wrap_content"
16:         android:textSize="24sp"
17:         android:padding="10dp" />
18: 
19:     <View
20:         android:layout_width="match_parent"
21:         android:layout_height="3dp"
22:         android:background="#d3d3d3" />
23: 
24:     <TextView
25:         android:id="@+id/textViewScoops"
26:         android:layout_width="match_parent"
27:         android:layout_height="wrap_content"
28:         android:textSize="20sp"
29:         android:padding="10dp"/>
30: 
31:     <View
32:         android:layout_width="match_parent"
33:         android:layout_height="1dp"
34:         android:background="#d3d3d3" />
35: 
36:     <TextView
37:         android:id="@+id/textViewFlavors"
38:         android:layout_width="match_parent"
39:         android:layout_height="wrap_content"
40:         android:textSize="20sp"
41:         android:padding="10dp"/>
42: 
43:     <View
44:         android:layout_width="match_parent"
45:         android:layout_height="1dp"
46:         android:background="#d3d3d3" />
47: 
48:     <TextView
49:         android:id="@+id/textViewContainer"
50:         android:layout_width="match_parent"
51:         android:layout_height="wrap_content"
52:         android:textSize="20sp"
53:         android:padding="10dp" />
54: 
55:     <View
56:         android:layout_width="match_parent"
57:         android:layout_height="1dp"
58:         android:background="#d3d3d3" />
59: 
60:     <TextView
61:         android:id="@+id/textViewDateTime"
62:         android:layout_width="match_parent"
63:         android:layout_height="wrap_content"
64:         android:textSize="20sp"
65:         android:padding="10dp" />
66: 
67:     <View
68:         android:layout_width="match_parent"
69:         android:layout_height="3dp"
70:         android:background="#d3d3d3" />
71: 
72:     <LinearLayout
73:         android:layout_width="match_parent"
74:         android:layout_height="0dp"
75:         android:layout_weight="1"
76:         android:gravity="bottom">
77: 
78:         <Button
79:             android:id="@+id/buttonCheckout"
80:             android:layout_width="match_parent"
81:             android:layout_height="wrap_content"
82:             android:text="@string/check_out_order"
83:             android:padding="10dp" />
84:     </LinearLayout>
85: 
86: </LinearLayout>

Beispiel 5. Klasse DetailsActivity: User Interface (XML)


001: package de.peterloos.petersicecreamparlor.activities;
002: 
003: import android.content.DialogInterface;
004: import android.support.v7.app.AlertDialog;
005: import android.support.v7.app.AppCompatActivity;
006: import android.os.Bundle;
007: import android.util.Log;
008: import android.view.View;
009: import android.widget.Button;
010: import android.widget.TextView;
011: import android.widget.Toast;
012: 
013: import com.google.firebase.database.DatabaseReference;
014: import com.google.firebase.database.FirebaseDatabase;
015: 
016: import java.text.DateFormat;
017: import java.util.Date;
018: import java.util.Locale;
019: 
020: import de.peterloos.petersicecreamparlor.Globals;
021: import de.peterloos.petersicecreamparlor.R;
022: import de.peterloos.petersicecreamparlor.parcels.OrderParcel;
023: 
024: public class DetailsActivity extends AppCompatActivity implements View.OnClickListener {
025: 
026:     private OrderParcel parcel;
027: 
028:     @Override
029:     protected void onCreate(Bundle savedInstanceState) {
030:         super.onCreate(savedInstanceState);
031:         this.setContentView(R.layout.activity_details_view);
032: 
033:         Log.v(Globals.TAG, "DetailsActivity::onCreate");
034: 
035:         // retrieve data from main activity
036:         this.parcel = null;
037:         Bundle data = this.getIntent().getExtras();
038:         if (data != null) {
039: 
040:             this.parcel = data.getParcelable(Globals.ORDER_PARCEL);
041:             if (this.parcel == null) {
042: 
043:                 Log.v(Globals.TAG, "Unexpected internal Error: parcel == NULL");
044:                 return;
045:             } else
046:                 Log.v(Globals.TAG, this.parcel.toString());
047:         }
048: 
049:         // retrieve references of controls
050:         TextView textViewHeader = this.findViewById(R.id.textViewHeader);
051:         TextView textViewScoops = this.findViewById(R.id.textViewScoops);
052:         TextView textViewFlavors = this.findViewById(R.id.textViewFlavors);
053:         TextView textViewContainer = this.findViewById(R.id.textViewContainer);
054:         TextView textViewDateTime = this.findViewById(R.id.textViewDateTime);
055:         Button buttonCheckout = this.findViewById(R.id.buttonCheckout);
056:         buttonCheckout.setOnClickListener(this);
057: 
058:         // fill text views with data
059:         String sHeader = "Order No. " + Long.toString(this.parcel.getPickupId());
060:         textViewHeader.setText(sHeader);
061: 
062:         String sScoops = String.format(
063:                 Locale.getDefault(),
064:                 "%d scoops",
065:                 this.parcel.getScoops()
066:         );
067:         textViewScoops.setText(sScoops);
068: 
069:         String sFlavors = "";
070:         String[] flavors = this.parcel.getFlavors();
071:         if (flavors.length == 1) {
072:             sFlavors = flavors[0];
073:         } else if (flavors.length == 2) {
074:             sFlavors = flavors[0] + " and " + flavors[1];
075:         } else if (flavors.length == 3) {
076:             sFlavors = flavors[0] + ", " + flavors[1] + " and " + flavors[2];
077:         }
078:         textViewFlavors.setText(sFlavors);
079: 
080:         String sContainer = "Container: " + this.parcel.getContainer();
081:         textViewContainer.setText(sContainer);
082: 
083:         long ts = parcel.getTimeStamp();
084:         Date date = new Date(ts);
085:         String s1 = DateFormat.getDateInstance().format(date);
086:         String s2 = DateFormat.getTimeInstance().format(date);
087:         String[] parts = s2.split(":");
088: 
089:         String date_time = "From " + s1 + " at " + parts[0] + ":" + parts[1];
090:         textViewDateTime.setText(date_time);
091:     }
092: 
093:     @Override
094:     public void onClick(View v) {
095: 
096:         AlertDialog.Builder builder = new AlertDialog.Builder(this);
097:         builder.setMessage("Checkout Order?");
098:         builder.setPositiveButton(
099:             R.string.yes_action,
100:             new DialogInterface.OnClickListener() {
101:                 public void onClick(DialogInterface dialog, int id) {
102: 
103:                     String msg = String.format(
104:                         Locale.getDefault(),
105:                         "Checking out order with id %d !",
106:                         DetailsActivity.this.parcel.getPickupId()
107:                     );
108: 
109:                     Toast.makeText(DetailsActivity.this, msg, Toast.LENGTH_LONG).show();
110: 
111:                     // check out this order: delete corresponding data in firebase backend
112:                     DetailsActivity.this.deleteOrder();
113: 
114:                     // navigate back to main activity
115:                     DetailsActivity.this.finish();
116:                 }
117:             }
118:         );
119:         builder.setNegativeButton(
120:                 R.string.no_action,
121:                 new DialogInterface.OnClickListener() {
122:                     public void onClick(DialogInterface dialog, int id) {
123:                         Toast.makeText(
124:                                 DetailsActivity.this,
125:                                 R.string.action_cancelled,
126:                                 Toast.LENGTH_LONG).show();
127:                     }
128:                 }
129:         );
130: 
131:         // create the AlertDialog object and show it
132:         builder.create();
133:         builder.show();
134:     }
135: 
136:     public void deleteOrder() {
137: 
138:         FirebaseDatabase database = FirebaseDatabase.getInstance();
139:         
140:         DatabaseReference ref = 
141:             database.
142:             getReference().
143:             child("orders").
144:             child(this.parcel.getKey());
145:         
146:         ref.removeValue();
147:     }
148: }

Beispiel 6. Klasse DetailsActivity: Logik-Anteil (Java).


Wie gelangen die Details einer Bestellung von einem MainActivity-Objekt zu einem DetailsActivity-Objekt? Primitive Datentypen lassen sich zwischen Activities einfach transferieren, es gibt zu diesem Zweck eine Klasse Bundle mit entsprechenden Hilfsmethoden zum Ein- und Auspacken elementarer Werte. Wie aber sieht es mit Objekten aus? Hier kommt die Schnittstelle Parcelable ins Spiel! Zum Zwecke des Transports eines Objekts von einer Activity zu einer zweiten Activity (für Profis: wir haben es an dieser Stelle mit IPC – Interprozesskommunikation, engl. Interprocess Communication – zu tun) benötigen wir eine Hilfsklasse, die diese Schnittstelle implementiert. Wie diese Hilfsklasse im Alltag eingesetzt wird, konnten Sie in Listing 5 und Listing 6 bereits erkennen. Allerdings fehlte noch ein Hinweis auf die Realisierung der Klasse OrderParcel, dies holen wir nun in Listing 7 nach:

001: package de.peterloos.petersicecreamparlor.parcels;
002: 
003: import android.os.Parcel;
004: import android.os.Parcelable;
005: import android.support.annotation.NonNull;
006: 
007: import java.util.Locale;
008: 
009: @SuppressWarnings("WeakerAccess")
010: public class OrderParcel implements Parcelable {
011: 
012:     // member data
013:     private String key;
014:     private long timeStamp;
015:     private long pickupId;
016:     private int scoops;
017:     private String[] flavors;
018:     private String container;
019: 
020:     // static field used to regenerate object, individually or as array
021:     public static final Parcelable.Creator<OrderParcel> CREATOR =
022:             new Parcelable.Creator<OrderParcel>() {
023:                 public OrderParcel createFromParcel(Parcel pc) {
024:                     return new OrderParcel(pc);
025:                 }
026: 
027:                 public OrderParcel[] newArray(int size) {
028:                     return new OrderParcel[size];
029:                 }
030:             };
031: 
032:     // system-defined c'tor from Parcel, reads back fields IN THE ORDER they were written
033:     public OrderParcel(Parcel pc) {
034:         this.readFromParcel(pc);
035:     }
036: 
037:     // user-defined c'tor
038:     public OrderParcel(String key, long timeStamp, long pickupId,
039:                        int scoops, String[] flavors, String container) {
040:         this.setKey(key);
041:         this.setTimeStamp(timeStamp);
042:         this.setPickupId(pickupId);
043:         this.setScoops(scoops);
044:         this.setFlavors(flavors);
045:         this.setContainer(container);
046:     }
047: 
048:     @Override
049:     public int describeContents() {
050:         return 0;
051:     }
052: 
053:     @Override
054:     public void writeToParcel(Parcel to, int flags) {
055:         to.writeString(this.getKey());
056:         to.writeLong(this.getTimeStamp());
057:         to.writeLong(this.getPickupId());
058:         to.writeInt(this.getScoops());
059:         to.writeStringArray(this.getFlavors());
060:         to.writeString(this.getContainer());
061:     }
062: 
063:     private void readFromParcel(Parcel in) {
064:         this.setKey(in.readString());
065:         this.setTimeStamp(in.readLong());
066:         this.setPickupId(in.readLong());
067:         this.setScoops(in.readInt());
068:         this.flavors = new String[this.getScoops()];
069:         in.readStringArray(this.getFlavors());
070:         this.setContainer(in.readString());
071:     }
072: 
073:     public String getKey() {
074:         return this.key;
075:     }
076: 
077:     public void setKey(String key) {
078:         this.key = key;
079:     }
080: 
081:     public long getTimeStamp() {
082:         return this.timeStamp;
083:     }
084: 
085:     public void setTimeStamp(long timeStamp) {
086:         this.timeStamp = timeStamp;
087:     }
088: 
089:     public long getPickupId() {
090:         return this.pickupId;
091:     }
092: 
093:     public void setPickupId(long pickupId) {
094:         this.pickupId = pickupId;
095:     }
096: 
097:     public int getScoops() {
098:         return this.scoops;
099:     }
100: 
101:     public void setScoops(int scoops) {
102:         this.scoops = scoops;
103:     }
104: 
105:     public String[] getFlavors() {
106:         return this.flavors;
107:     }
108: 
109:     public void setFlavors(String[] flavors) {
110:         this.flavors = flavors;
111:     }
112: 
113:     public String getContainer() {
114:         return this.container;
115:     }
116: 
117:     public void setContainer(String container) {
118:         this.container = container;
119:     }
120: 
121:     @Override
122:     @NonNull
123:     public String toString() {
124:         return this.print();
125:     }
126: 
127:     @SuppressWarnings("unused")
128:     private String print() {
129:         StringBuilder sb = new StringBuilder();
130:         sb.append(String.format(Locale.getDefault(), "PickupId: %d", this.pickupId));
131:         sb.append(" - ");
132:         sb.append(String.format(Locale.getDefault(), "TimeStamp: %d", this.timeStamp));
133:         sb.append(" - ");
134:         sb.append(String.format(Locale.getDefault(), "Num Scoops: %d", this.scoops));
135:         sb.append(" - ");
136:         sb.append(String.format(Locale.getDefault(), "Container: %s", this.container));
137:         sb.append(" - ");
138: 
139:         if (this.flavors == null) {
140:             sb.append("flavors == null ");
141:             sb.append(" - ");
142:         } else {
143:             sb.append("Flavors:");
144:             sb.append(" - ");
145:             for (int i = 0; i < this.flavors.length; i++) {
146:                 sb.append(String.format(Locale.getDefault(),
147:                         "   %d: %s", (i + 1), this.flavors[i]));
148:             }
149:         }
150: 
151:         return sb.toString();
152:     }
153: }

Beispiel 7. Klasse OrderParcel: Hilfsklasse für Android IPC.


Um Verwechslungen zu vermeiden: Die Klasse OrderModel ist eine Hilfsklasse für Firebase-Clients: Mit ihrer Hilfe lassen sich Nachrichtenpakete im JSON-Format in Java-Objekte umwandeln. Die Klasse OrderParcel wiederum kommt ins Spiel, wenn in einer App Java-Objekte zwischen Activities transportiert werden sollen.

Damit sind wir am Ende der Betrachtungen unseres „Proof of Concepts“ angekommen. Die Quellen der App finden Sie in diesem Github-Repository vor, viel Spaß beim Stöbern im Quellcode.