Angepinnt Eine App entsteht

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

  • Eine App entsteht

    tl;dr:
    Repository URL: github.com/marcofeltmann/TimeTracker/
    Fragen, Wünsche, Anregungen: Fragen zum Projekt 'Eine App entsteht'

    Hallo,

    angestachelt von der Frage nach einem Zeiterfassungssystem möchte ich das nicht nur so umsetzen, wie ich es für richtig halte, sondern auch gleich noch aufzeigen, wie ich an so eine Sache herangehe.

    Dazu vorab ein paar Informationen für Interessierte.

    • Kenntnisse in der Android Entwicklung sind grundlegende Voraussetzung. Du hast idealerweise schon ein paar kleinere Tools erstellt, kennst Dich ein bisschen mit den Unterschieden zwischen Tablet und Smartphone aus, bist froh dass Android 2.x nahezu ausgestorben ist und suchst Inspirationen mit Deinem Wunschprogramm loszulegen.
    • Dementsprechend gibt es keinerlei Einführung in die Installation und Konfiguration von IDE, Testgeräten, des SDK oder der Veröffentlichung von Apps.
    • Ich bin kein UI Designer und habe auch keinen zur Hand. Insofern versuche ich zwar, das Ganze halbwegs nichthässlich aussehen zu lassen, kann aber für nix garantieren.
    • Kenntnisse von Git im Besonderen und Source Code Management im Allgemeinen sind zwingend notwendig, um die Beispiele sinnvoll nutzen zu können.
      Einen Git Crashkurs gibt es unter guides.github.com/activities/hello-world/
    • Ich werde den Git Flow Workflow verwenden, aber veraltete Branches nicht löschen. So könnt ihr euch zeitmaschienenmäßig vor und zurück bewegen.
    • Ihr findet alle relevanten Dateien im Git Repository. Was nicht drin liegt war auch nie relevant.


    Soweit zu den Vorworten, legen wir gleich los.

    Und zwar mit einer Designentscheidung.
    Je mehr Käse, desto mehr Löcher.
    Je mehr Löcher, desto weniger Käse.
    Daraus folgt: je mehr Käse, desto weniger Käse.

    »Dies ist ein Forum. Schreibt Eure Fragen in das Forum, nicht per PN!«

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von Marco Feltmann ()

  • Bevor wir nun anfangen irgendwelchen Code zu tippen, UIs zu designen, uns in Ideen zu verrennen und ein schwaches Produkt abzuliefern machen wir uns erst einmal Gedanken darüber, was die App können soll.

    Und zwar einen Schritt nach dem Anderen.

    In erster Linie soll die App unsere Kommen- und Gehen Zeiten aufzeichnen.
    Nichts leichter als das, da fällt mir sogar schon ein Layout für ein - doch was ist, wenn das Layout zu kompliziert wird?
    Wenn die App dadurch schwer zu nutzen ist? Niemand möchte sich in seiner Produktivität gebremst fühlen.

    Wie also bekommen wir heraus, ob unser Layout funktioniert ohne es zu programmieren und auszuprobieren (und dann schlussendlich bei der weniger guten Lösung zu bleiben, weil wir da schon drei Stunden dran gefeilt haben)?

    Die "Bösen" aus Cupertino predigen seit Jahren "Fake It 'Til You Make It". Ihre Präsentationsapp kann auf iPhone Größe eingestellt werden und speziell zugeschnittene Präsentationen auf dem Gerät so wiedergeben, als wäre man in der App - die es gar nicht gibt.

    Ich bediene mich Googles Präsentationsdienst, weil sich die Sachen hier prima verlinken lassen und so wirklich schützenswert sind die Ideen eh nicht.
    Also frisch ans Werk!

    Entwurf #1 ist fertig.
    (Auf dem iPhone würde jetzt nach jedem Tab der nächste Screen kommen, tabbt man also ungefähr auf die Eingabeelemente bekommt man ein Gefühl dafür, wie das Ganze laufen könnte.)

    Hier sehen wir gleich, das Konzept taugt nicht. Tab, eingeben, Tab, eingeben, dynamisch wachsende Liste - alles sehr unübersichtlich.
    Eigentlich brauchen wir die Details auch gar nicht auf dem ersten Screen, Auswertungen und Bearbeiten soll über eigene Screens geschehen.

    Entwurf #2 ist fertig.
    Irgendwie noch nicht das Gelbe vom Ei. Wir zeigen zwar die notwendigen Informationen, aber an Hand der Screens ist ersichtlich, das niemand weiß in welchem Modus er gerade ist.

    Schöner wäre es, der Button wüsste was wir vorhaben.

    Entwurf #3 gefällt mir da am Besten.
    An Hand des Buttons erkennt man, ob man gerade angekommen (Dreieck) oder gegangen (Kreis) ist.

    Ich kann mich nur nicht entscheiden, ob relative Dauer (Noch 6 Stunden...) oder absolute Uhrzeit (Mindestens bis 14:00 Uhr...) sinnvoller ist.
    Aber ich gehe davon aus, dass diese Vorliebe vom Nutzer abhängt. Also wird das in die Settings übernommen.
    (Genauso wie Wochenarbeitszeit, Arbeitstage die Woche, zulässige prozentuale Anzahl Überstunden... Doch dazu später mehr.)

    Da wir nun ungefähr wissen, wie das UI funktionieren soll, wäre es doch prima, wir fingen gleich damit an.
    Je mehr Käse, desto mehr Löcher.
    Je mehr Löcher, desto weniger Käse.
    Daraus folgt: je mehr Käse, desto weniger Käse.

    »Dies ist ein Forum. Schreibt Eure Fragen in das Forum, nicht per PN!«
  • Nein, immer noch kein Code.
    Erst testen, dann bauen. :)

    Außerdem ist es schon spät...

    Ihr könnt euch ja schon mal mit den Testing Trainings von Android vertraut machen.
    Da wir zunächst nur testen werden, ob der Button sich bei einem Klick ändert, beginnen wir mit UI Tests.
    developer.android.com/training/testing/ui-testing/index.html
    Je mehr Käse, desto mehr Löcher.
    Je mehr Löcher, desto weniger Käse.
    Daraus folgt: je mehr Käse, desto weniger Käse.

    »Dies ist ein Forum. Schreibt Eure Fragen in das Forum, nicht per PN!«
  • Sooo. Für sämtliche Verzögerungen entschuldige ich mich schon mal im Voraus.
    (Mein ordentliches Notebook mit einem reinen 64 Bit Linux kann nicht zur Entwicklung genutzt werden, da Teile des SDK zwingend 32 Bit erfordern und die notwendigen Bibliotheken nicht in 32 Bit bei der Distribution vorliegen. Also quäle ich mich mit Windows 10 auf 1.33GHz, 2GB RAM und 32GB SD rum. Kann man mit arbeiten, wenn man auf AVDs verzichtet. Dauert allerdings alles ein wenig länger.)

    Um mit dem UI Test zu beginnen, brauche ich persönlich nicht einmal ein existierendes UI, obwohl natürlich eine Defaultactivity angelegt wurde.
    Ich teste immer erst alles, bevor ich es baue.

    Klingt unlogisch, denn der Test kann ja nur fehlschlagen, wenn ich noch nichts programmiert habe. Aber so kann ich sehen, dass mein Test auch wirklich ausgeführt wird. Fehlschlagende Tests sind kein Problem, solange sie irgendwann wieder laufen. Nur vor Auslieferung müssen alle Tests durchlaufen.

    Zunächst fange ich mit drei Tests an:
    • Ist auch wirklich die richtige Activity offen?
    • Verhält sich der 'Kommen' Knopf so, wie ich es erwarte?
    • Verhält sich der 'Gehen' Knopf so, wie ich es erwarte?


    Wie ihr euch vorstellen könnt, machen die implementierten Tests (Branch 01UITest) noch nichts sinnvolles. Die wichtigen Ausgaben aus dem Log:

    java.lang.AssertionError: Not Yet Implemented
    at de.marcofeltmann.coding.timetracker.TimeRecordingActivitiyUITest.startButtonPressed(TimeRecordingActivitiyUITest.java:32)

    java.lang.AssertionError: Not Yet Implemented
    at de.marcofeltmann.coding.timetracker.TimeRecordingActivitiyUITest.endButtonPressed(TimeRecordingActivitiyUITest.java:38)

    java.lang.AssertionError: Not Yet Implemented
    at de.marcofeltmann.coding.timetracker.TimeRecordingActivitiyUITest.correctActivityOpened(TimeRecordingActivitiyUITest.java:26)

    22:48:52 Tests Failed: 0 passed, 3 failed


    Sieht nach unnützen Informationen aus, aber ich sehe, dass die Tests aufgerufen werden und funktionieren (a.k.a. fehlschlagen).

    Nun fange ich an zu prüfen, ob die Activity die richtige ist. Mangels Kreativität vergleiche ich den Titel der Activity mit dem hinterlegten Titel in der Sprachdatei.
    So vermeide ich auch Probleme beim Testen auf unterschiedlich lokalisierten Geräten.

    Zusammengezimmert sieht das Ganze dann so aus:

    Java-Quellcode

    1. @Test
    2. public void correctActivityOpened() {
    3. String activityTitle = mActivityRule.getActivity().getTitle().toString();
    4. assertEquals( mActivityRule.getActivity().getResources().getString(R.string.app_name), activityTitle );
    5. }

    22:51:33 Tests Failed: 1 passed, 2 failed

    Und schon ist 1/3 der Fehler beseitigt. :)

    Wahnsinn, seit ungefähr 3 Stunden gewerkelt, Null Produktivcode geschrieben und schon einen "Fehler" beseitigt. Wenn das nur immer so einfach wäre.
    Ist es aber natürlich nicht. Denn jetzt geht es um die Überlegung, wie die Buttons implementiert werden.
    Ja, die Buttons.

    Ich weiß nach wie vor nur, dass es prima ist, wenn der Button seine Form entsprechend ändert, ich weiß aber nicht, ob er seine Position ändern soll. Auch finde ich es übersichtlicher, wenn ein Button für genau eine Aktion da ist. Es ist auch weniger fehleranfällig, als das Austauschen von gesendeten Nachrichten zur Laufzeit. Hinzu kommt, dass auch blinde Menschen die App nutzen können sollen.
    (Ich kenne mehrere blinde Personen, die voll erwerbstätig sind und ein Smartphone benutzen. Meist ein iPhone, aber das liegt sicherlich nur daran, dass die 08-15 Entwickler unter Android Sehbehinderte einfach ignorieren.)
    Entsprechend tausche ich also nicht nur Aussehen und Nachricht des Buttons, sondern auch noch seinen via Talkback ausgegebenen Hinweistext.
    Das sind dann viel zu viele Änderungen, die im Nachhinein nur für Verwirrung sorgen und das ganze Programm anfällig für Flüchtigkeitsfehler machen.

    Also, da ich weiß, dass ich zwei Buttons habe und auch ungefähr weiß, wie ich diese Buttons nennen werde, kann ich auch die beiden Tests fertigstellen.

    Java-Quellcode

    1. @Test
    2. public void startButtonPressed() {
    3. ViewInteraction enterWorkButtonInteraction = onView( withId( R.id.enterWorkButton ) );
    4. enterWorkButtonInteraction.perform( click() );
    5. enterWorkButtonInteraction.check( matches( isDisplayed() ) );
    6. onView( withId( R.id.leaveWorkButton ) ).check( doesNotExist() );
    7. }
    8. @Test
    9. public void endButtonPressed() {
    10. ViewInteraction leaveWorkButtonInteraction = onView( withId( R.id.leaveWorkButton ) );
    11. leaveWorkButtonInteraction.perform( click() );
    12. leaveWorkButtonInteraction.check( matches( isDisplayed() ) );
    13. onView( withId( R.id.enterWorkButton ) ).check( doesNotExist() );
    14. }
    Alles anzeigen

    Damit das kompiliert dürfen wir endlich ein UI erstellen. Aber wirklich nur zwei Buttons $irgendwo mit den beiden IDs vergeben.
    An der Positionierung wird erst geschraubt wenn wir wissen, wie sie sein soll!

    Nachdem das UI so kurz erledigt ist können wir das gewünschte zu testende Verhalten implementieren.
    OnClick Listener, die einfach sich selbst sichtbar und 'den Anderen' unsichtbar machen sollten reichen.

    Im zugehörigen Branch könnt ihr auch nachverfolgen, wie das Refactoring so vor sich ging. 8)
    Je mehr Käse, desto mehr Löcher.
    Je mehr Löcher, desto weniger Käse.
    Daraus folgt: je mehr Käse, desto weniger Käse.

    »Dies ist ein Forum. Schreibt Eure Fragen in das Forum, nicht per PN!«
  • Gut 24 Stunden nach dem ersten und 3 Stunden nach dem vorherigen Eintrag ist das Projekt so weit, dass diese doch recht simplen Tests reproduzierbar erfolgreich sind.

    Es ging mal wieder einiges schief. Die Syntax zum Feststellen der Nichtsichtbarkeit war falsch, beim Starten wurde der 'Gehen' Button noch mit angezeigt und die App setzte jedes Mal nach Beenden und neu starten seinen Buttonstatus zurück, so dass immer nur 'Kommen' angezeigt wurde und vormals laufende Tests fehl schlugen.
    Ich hätte die Tests natürlich dahingehend umbauen können, dass alle Buttoninteraktionen in einem Test abgearbeitet werden.
    Vielleicht mache ich das auch noch, um einen Stresstest zu simulieren oder so.
    In diesem Fall sehe ich aber ein, dass während so einem Arbeitstag eine Menge Dinge auf dem Endgerät passieren können, so dass die App durchaus auch mal weggeräumt werden kann.
    Also teste und implementiere ich das gleich richtig.

    So habe ich mit einem simplen UI Test auch gleich noch eine wichtige Kernfunktion implementiert, die auf dem ersten Blick nichts mit dem UI zu tun hatte.
    Hier sieht man auch, dass automatische UI Tests nur so gut sind wie sie geschrieben wurden. Es muss also nach wie vor im Entwicklungsprozess alles genau manuell getestet werden - danach dann aber nicht mehr.

    --

    Leider musste ich feststellen, dass GitHub nicht die gewünschte Oberflächengestaltung hat, wie sie beispielsweise BitBucket liefert.
    Das ist ärgerlich, denn so bekommt ihr nur über Umwege einen Überblick darüber, welche Änderungen an den Dateien vorgenommen wurden.

    Immerhin können die GitHub Desktop App, SourceTree oder GitKraken das ganze veranstaltete Chaos einigermaßen übersichtlich darstellen.

    Der große Vorteil dergestalter Darstellungen ist, dass sich niemand durch die kompletten Sourcen kämpfen muss, um die Änderungen selbst zu suchen sondern sie werden aufbereitet dargestellt.
    Beispielhafte Screenshots der Apps anbei, so könnt ihr euch ein eigenes Bild davon machen und entscheiden, ob ihr eine der genannten Apps nutzen wollt.

    GitHub App


    SourceTree


    GitKraken
    Je mehr Käse, desto mehr Löcher.
    Je mehr Löcher, desto weniger Käse.
    Daraus folgt: je mehr Käse, desto weniger Käse.

    »Dies ist ein Forum. Schreibt Eure Fragen in das Forum, nicht per PN!«

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von Marco Feltmann ()

  • Auf zur nächsten Runde!
    Hierfür verlassen wir die Benutzeroberfläche und kümmern uns um das Kernstück der Anwendung: Die Zeiterfassung selbst.

    Im Prinzip muss diese Komponente ja nichts Anderes tun als die Sekunden zählen, die zwischen 'Kommen' und 'Gehen' verstreichen.
    Doch ist das wirklich eine so gute Idee? Stellen wir uns das kurz einmal vor: Jede Sekunde wird ein Zähler um 1 erhöht.
    Das sind dann 3.600 Berechnungen die Stunde bzw. ganze 28.800 Berechnungen bei einem 8 Stunden Tag. Sicherlich nicht viel für so ein Gerät, das derartige Berechnungen auch in wenigen Sekunden durchführen kann. Beispielsweise für Animationen, Videos, Spiele und so weiter. Nur gehen gerade Spiele und Videos sehr schnell den Akku an. Wenn das also nun jede App machen würde...

    Wir müssen auch die Unterbrechungen berücksichtigen. Kurz jemanden anrufen, Mails abrufen, dem Kollegen via WhatsApp eine Sprachnachricht schicken, dem Controlling im internen Chat eine Frage stellen, eine Kleinigkeit im Browser suchen, das gerade herumgemailte PDF ansehen... und unsere App wurde aus dem Speicher geräumt.
    Da läuft dann natürlich nichts weiter, der Zähler zählt nicht, die Daten sind falsch.
    Natürlich könnten wir einen Service anbinden, der im Hintergrund die Zeit mitzählt und die App kann dann darauf zugreifen. Aber gibt es diese Services nicht schon zu Hauf?
    Uhr. Stoppuhr. Eieruhr. Wecker.
    Ist es wirklich eine gute Idee, noch so ein Ding ins System zu packen?

    Schauen wir uns dazu einmal den Vorgänger der elektronischen Zeiterfassung an:
    [Blockierte Grafik: https://upload.wikimedia.org/wikipedia/commons/e/e4/Industriemuseum_Chemnitz_-_Stechuhr_der_W%C3%BCrttembergischen_Uhrenfabrik_B%C3%BCrk_%28um_1930%29.jpg]
    Okay, ist ne Uhr die permanent mitläuft. Unser Ansatz stimmt also. Schauen wir uns zur Sicherheit noch einmal das personalisierte Gegenstück an.
    [Blockierte Grafik: http://www.megzeit.de/_pics/830/_605x379_1_0_0_0x0_ffffff/MAX-ER-1500-Stempelkarte.jpg]
    Die alten Systeme haben also gar nicht permanent gezählt, sondern einfach nur den jeweils aktuellen Zeitpunkt gestempelt.
    Zeitpunkt gestempelt... Zeitstempel... Timestamp... Das kenne ich doch von irgendwo her... Timestamp in MSSQL
    Das Rad muss also nicht neu erfunden werden, irgendwo gibt es das schon.
    java.sql.Timestamp Gut, dafür jetzt SQL importieren, ich weiß nicht recht...
    java.lang.System.currentTimeMillis() Na bitte, das sieht doch prima aus. So ein Aufruf aus dem Sprachsystem kostet auch kaum Zeit bzw. liegt die Effizienz des Ganzen nicht in unserem Aufgabenbereich.

    Nun noch einmal die Karte ansehen, bevor wir loslegen.
    Sie stempelt Stunden und Minuten untereinander weg. Die Zeilen stehen wohl für das Datum. Das lässt sich ja an Hand des Zeitstempels wunderbar errechnen.
    Andererseits wird dieses hin- und herwandeln der Millisekunden in Datumsobjekte und die Berechnung von Zeitdifferenzen vielleicht ein bisschen viel Arbeit.
    Nein, den Spielkram mit den Millisekunden verwerfen wir wieder. Nehmen wir am Besten direkt java.util.Date.

    Mit diesem Wissen bewaffnet geht es sofort ans Werk, wir... Ja, was eigentlich?
    Da braucht nix im Hintergrund mitlaufen. Fürs Stempeln an sich müssen wir nichts berechnen. Eigentlich können wir den Kram doch auch gleich in die Methoden des Button stecken.
    Doch halt, wenn ich überhaupt keinen Bock habe jedes mal diese verfluchte App zu öffnen um mich zu stempeln? Wenn ich mir lieber ein Widget bauen möchte, das ganz unauffällig auf dem Homescreen liegt und mich auf Tastendruck ein- und ausstempeln soll?

    Das schreit geradezu nach einem Content Provider!

    Stimme aus dem Off schrieb:

    Moooooment mal. Einen Contentprovider? Nicht erst einmal eine Klasse und dann mal gucken was passiert und so? Test First!

    Jaaa, nee. Ich habe ja so ein bisschen im Hinterkopf, dass meine App diese Zeitstempel auch irgendwo lassen soll. Dass aus den generierten Zeitstempeln Berichte, Übersichten und Exporte erstellt werden sollen. Dass sie gesichert werden sollen. Auch im Backup auf einem Rechner oder synchronisiert mit Google.
    Es ist also bereits zu diesem frühen Punkt ersichtlich, dass es auf eine SQLite Datenbank hinaus läuft. Genau (aber nicht ausschließlich) dafür sind Content Provider bestens geeignet und wir werden im Laufe der Programmentwicklung auch sehen warum. Vor Allem werden wir es an den Unit Tests erkennen, die hier zum Einsatz kommen werden.
    Je mehr Käse, desto mehr Löcher.
    Je mehr Löcher, desto weniger Käse.
    Daraus folgt: je mehr Käse, desto weniger Käse.

    »Dies ist ein Forum. Schreibt Eure Fragen in das Forum, nicht per PN!«