Gepostet von Ekaterina Semashko, Starke Junior iOS-Entwicklerin, DataArtEin wenig über das Projekt: eine mobile Anwendung für die iOS-Plattform, geschrieben in Swift. Der Zweck der Anwendung ist die Möglichkeit, Rabattkarten zwischen Mitarbeitern des Unternehmens und ihren Freunden zu teilen.
Eines der Ziele des Projekts war es, beliebte Technologien und Bibliotheken zu lernen und zu üben. Realm wurde zum Speichern lokaler Daten ausgewählt, Alamofire wurde für die Arbeit mit dem Server verwendet, Google Sign-In wurde zur Authentifizierung verwendet, PINRemoteImage wurde zum Hochladen von Bildern verwendet.
Die Hauptfunktionen der Anwendung:
- Hinzufügen einer Karte, Bearbeiten und Löschen;
- Anzeigen der Karten anderer Personen;
- Suche nach Karten nach Geschäftsname / Benutzername;
- Fügen Sie Ihren Favoriten Karten für den schnellen Zugriff hinzu.
Die Möglichkeit, die Anwendung ohne Verbindung zum Netzwerk zu verwenden, wurde von Anfang an vorausgesetzt, jedoch nur im Lesemodus. Das heißt, Wir konnten Informationen über Karten anzeigen, aber ohne das Internet nicht ändern. Zu diesem Zweck verfügte die Anwendung immer über eine Kopie aller Karten und Marken der Datenbank vom Server sowie über eine Liste der Favoriten für den aktuellen Benutzer. Die Suche wurde auch lokal implementiert.
Später beschlossen wir, offline durch Hinzufügen eines Aufnahmemodus zu erweitern. Informationen zu vom Benutzer vorgenommenen Änderungen wurden gespeichert und synchronisiert, als eine Internetverbindung hergestellt wurde. Die Implementierung eines solchen Lese- / Schreib-Offline-Modus wird diskutiert.
Was ist für einen vollständigen Offline-Modus in einer mobilen Anwendung erforderlich? Wir müssen die Abhängigkeit des Benutzers von der Qualität der Internetverbindung beseitigen, insbesondere:
- Entfernen Sie die Abhängigkeit der Antworten des Benutzers von seinen Aktionen in der Benutzeroberfläche vom Server. Zunächst wird die Anforderung mit dem lokalen Speicher interagiert und dann an den Server gesendet.
- Lokale Änderungen markieren und speichern.
- Implementieren eines Synchronisationsmechanismus - Wenn eine Internetverbindung angezeigt wird, müssen Sie Änderungen an den Server senden.
- Zeigen Sie dem Benutzer, welche Änderungen synchronisiert sind, welche nicht.
Offline-First-Ansatz
Zunächst musste ich den vorhandenen Mechanismus für die Interaktion mit dem Server und der Datenbank ändern. Ziel war es, den Benutzer daran zu hindern, von der Anwesenheit oder Abwesenheit des Internets abhängig zu sein. Zunächst sollte es mit dem lokalen Data Warehouse interagieren und Serveranforderungen sollten im Hintergrund stehen.
In der vorherigen Version bestand eine starke Verbindung zwischen der Datenspeicherschicht und der Netzwerkschicht. Der Mechanismus für die Arbeit mit Daten war wie folgt: Zuerst wurde über die NetworkManager-Klasse eine Anforderung an den Server gestellt, wir haben auf das Ergebnis gewartet, danach wurden die Daten über die Repository-Klasse in der Datenbank gespeichert. Dann wurde das Ergebnis an die Benutzeroberfläche übergeben, wie im Diagramm gezeigt.

Um den Offline-First-Ansatz zu implementieren, habe ich die Datenspeicherschicht und die Netzwerkschicht getrennt und eine neue Flow-Klasse eingeführt, die die Reihenfolge steuert, in der NetworkManager und Repository aufgerufen wurden. Jetzt werden die Daten zuerst über die Repository-Klasse in der Datenbank gespeichert, dann wird das Ergebnis an die Benutzeroberfläche gesendet und der Benutzer arbeitet weiterhin mit der Anwendung. Im Hintergrund wird eine Anforderung an den Server gesendet. Nach der Antwort werden die Informationen in der Datenbank und der Benutzeroberfläche aktualisiert.
Arbeiten Sie mit Objektkennungen
Mit der neuen Architektur wurden mehrere neue Aufgaben angezeigt, von denen eine mit ID-Objekten arbeitet. Zuvor haben wir sie beim Erstellen des Objekts vom Server erhalten. Jetzt wurde das Objekt lokal erstellt. Dementsprechend musste eine ID generiert und nach der Synchronisierung auf die aktuellen aktualisiert werden. Hier bin ich auf die erste Einschränkung von Realm gestoßen: Nach dem Erstellen eines Objekts können Sie dessen Primärschlüssel nicht mehr ändern.
Die erste Option bestand darin, den Primärschlüssel im Objekt aufzugeben und id zu einem regulären Feld zu machen. Gleichzeitig gingen jedoch die Vorteile der Verwendung des Primärschlüssels verloren: Realm-Indizierung, die das Abrufen des Objekts beschleunigt, die Möglichkeit, das Objekt mit dem Flag create zu aktualisieren (ein Objekt erstellen, falls es nicht vorhanden ist) und die Einhaltung der Eindeutigkeit des Objekts.
Ich wollte den Primärschlüssel speichern, aber es konnte nicht die ID des Objekts vom Server sein. Infolgedessen bestand die funktionierende Lösung darin, zwei Bezeichner zu haben, einen Server, ein optionales Feld und den zweiten lokalen Bezeichner, der der Primärschlüssel sein würde.
Infolgedessen wird beim lokalen Erstellen des Objekts auf dem Client eine lokale ID generiert. Wenn das Objekt vom Server stammt, entspricht es der Server-ID. Da es in der einzigen Quelle der Wahrheitsanwendung eine Datenbank gibt, wird das Objekt beim Empfang von Daten vom Server mit der aktuellen lokalen Kennung aktualisiert und arbeitet nur damit. Beim Senden von Daten an den Server wird die Server-ID übertragen.
Speicherung nicht synchronisierter Änderungen
Änderungen an Objekten, die noch nicht an den Server gesendet wurden, müssen lokal gespeichert werden. Dies kann auf folgende Arten implementiert werden:
- Hinzufügen von Feldern zu vorhandenen Objekten
- Speichern nicht synchronisierter Objekte in separaten Tabellen;
- Speichern einzelner Feldänderungen in einem bestimmten Format.
Ich verwende Realm-Objekte nicht direkt in meinen Klassen, sondern mache ihre Zuordnung selbst, um Probleme mit Multithreading zu vermeiden. Die automatische Aktualisierung der Benutzeroberfläche erfolgt anhand von Beispielen für die automatische Aktualisierung der Ergebnisse, bei denen ich Aktualisierungsanforderungen abonniere. Nur der erste Ansatz funktionierte mit meiner aktuellen Architektur, daher fiel die Wahl auf das Hinzufügen von Feldern zu vorhandenen Objekten.
Das Kartenobjekt hat die meisten Änderungen erfahren:
- synchronisiert - Gibt es Daten auf dem Server?
- gelöscht - true, wenn die Karte nur lokal gelöscht wird, ist eine Synchronisierung erforderlich.
Bezeichner, die im vorherigen Teil besprochen wurden:
- localId - der Primärschlüssel der Entität in der Anwendung, entweder gleich der Server-ID oder lokal generiert;
- serverId - ID vom Server.
Besonders erwähnenswert ist die Speicherung von Bildern. Im Wesentlichen wurde das Anhangsfeld diskURL zum serverURL-Feld des Images auf dem Server hinzugefügt, in dem die Adresse des lokalen nicht synchronisierten Images gespeichert ist. Bei der Synchronisierung des Bildes wurde das lokale Bild gelöscht, um den Speicher des Geräts nicht zu verstopfen.
Serversynchronisierung
Um mit dem Server zu synchronisieren, wurde die Arbeit mit Erreichbarkeit hinzugefügt, sodass der Synchronisierungsmechanismus gestartet wird, wenn das Internet angezeigt wird.
Zunächst wird geprüft, ob Änderungen an der Datenbank vorliegen, die übermittelt werden müssen. Anschließend wird eine Anforderung an den Server für eine tatsächliche Datenübertragung gesendet. Infolgedessen werden Änderungen, die nicht an den Client gesendet werden müssen, herausgefiltert (z. B. eine Änderung an einem Objekt, das bereits auf dem Server gelöscht wurde). Die verbleibenden Änderungen stellen Anforderungen an den Server in die Warteschlange.
Um Änderungen zu senden, war es möglich, Massenaktualisierungen zu implementieren, die Änderungen in einem Array zu senden oder eine große Anforderung zum Synchronisieren aller Daten zu stellen. Zu diesem Zeitpunkt war der Backend-Entwickler jedoch bereits mit einem anderen Projekt beschäftigt und half uns nur in unserer Freizeit. Daher erstellen wir eine Anfrage für jede Art von Änderung.
Ich habe die Warteschlange über OperationQueue implementiert und jede Anforderung in eine asynchrone Operation eingeschlossen. Einige Operationen hängen voneinander ab. Beispielsweise können wir das Kartenbild vor dem Erstellen der Karte nicht laden. Daher habe ich die Abhängigkeit der Bildoperation zur Kartenoperation hinzugefügt. Außerdem hatte das Hochladen von Bildern auf den Server eine niedrigere Priorität als alle anderen, und ich habe sie aufgrund ihrer Schwere auch zuletzt in die Warteschlange aufgenommen.
Bei der Planung des Offline-Modus bestand die große Frage darin, Konflikte mit dem Server während der Synchronisierung zu lösen. Als wir jedoch während der Implementierung an diesen Punkt kamen, stellten wir fest, dass der Fall, dass ein Benutzer dieselben Daten auf verschiedenen Geräten ändert, sehr selten ist. Es reicht uns also, den Mechanismus für die letzten Writer-Gewinne zu implementieren. Während der Synchronisierung haben nicht gesendete Änderungen auf dem Client immer Vorrang, sie werden nicht gerieben.
Die Fehlerbehandlung steckt noch in den Kinderschuhen. Wenn die Synchronisierung fehlschlägt, wird das Objekt beim nächsten Erscheinen des Internets zur Änderungswarteschlange hinzugefügt. Und wenn es nach dem Zusammenführen immer noch nicht synchron ist, entscheidet der Benutzer, ob er es verlassen oder löschen möchte.
Zusätzliche Problemumgehung bei der Arbeit mit Realm
Bei der Arbeit mit Realm gab es einige weitere Probleme. Vielleicht ist diese Erfahrung auch für jemanden nützlich.
Bei der Sortierung nach Zeichenfolge richtet sich die Reihenfolge nach der Zeichenreihenfolge in UTF-8. Es gibt keine Unterstützung für die Suche zwischen Groß- und Kleinschreibung. Wir sind mit einer Situation konfrontiert, in der die Namen in Kleinbuchstaben nach den Namen in Großbuchstaben stehen, zum Beispiel: Magnet, Pyaterochka, Ribbon. Wenn die Liste sehr groß ist, werden alle Namen in Kleinbuchstaben unten angezeigt, was sehr unangenehm ist.
Um die Sortierreihenfolge unabhängig von der Groß- und Kleinschreibung beizubehalten, mussten wir ein neues Feld für den Namen mit niedrigerem Fall einführen, es beim Aktualisieren des Namens aktualisieren und danach sortieren.
Außerdem wurde ein neues Feld zum Sortieren nach dem Vorhandensein einer Karte in den Favoriten hinzugefügt, da dies im Wesentlichen eine Unterabfrage für die Beziehungen des Objekts erfordert.
Bei der Suche in Realm gibt es die CONTAINS [c]% @ -Methode für Suchvorgänge ohne Berücksichtigung der Groß- und Kleinschreibung. Aber leider funktioniert es nur mit dem lateinischen Alphabet. Für russische Marken mussten wir auch separate Felder erstellen und danach suchen. Später stellte sich heraus, dass es in unseren Händen lag, Sonderzeichen bei der Suche auszuschließen.
Wie Sie sehen können, ist es für mobile Anwendungen durchaus möglich, einen Offline-Modus zu implementieren, in dem Änderungen gespeichert und mit wenig Blut synchronisiert werden, manchmal sogar mit minimalen Änderungen im Backend.
Trotz einiger Schwierigkeiten können Sie Realm verwenden, um es zu implementieren, und gleichzeitig alle Vorteile in Form von Live-Updates, einer Nullkopie-Architektur und einer praktischen API erhalten.
Es gibt also keinen Grund, Ihren Benutzern jederzeit den Zugriff auf Daten zu verweigern, unabhängig von der Qualität der Verbindung.