Das Freigeben von Anwendungen für nur eine mobile Plattform ist nicht relevant und Sie müssen darauf achten, zwei Versionen gleichzeitig für iOS und Android zu entwickeln. Hier können Sie zwei Möglichkeiten wählen: Arbeiten Sie in den "nativen" Programmiersprachen für jedes Betriebssystem oder verwenden Sie plattformübergreifende Frameworks.
Bei der Entwicklung eines der Projekte bei DD Planet habe ich mich auf die letzte Option verlassen. In diesem Artikel werde ich über die Erfahrungen bei der Entwicklung einer plattformübergreifenden Anwendung, die aufgetretenen Probleme und die gefundenen Lösungen sprechen.
Drei Ansätze zur Entwicklung plattformübergreifender mobiler Anwendungen
Überlegen Sie zunächst, welche Ansätze verwendet werden, wenn Sie zwei Anwendungen gleichzeitig benötigen: für iOS und Android.
Die erste ist zeitlich und ressourcenintensivste: Entwicklung einer separaten Anwendung für jede Plattform. Die Komplexität dieses Ansatzes liegt in der Tatsache, dass jedes Betriebssystem seinen eigenen Ansatz erfordert: Dies wird sowohl in der Sprache ausgedrückt, in der die Entwicklung läuft (für Android - Java oder Kotlin, für iOS - Objective-C oder Swift), als auch in Methoden zur Beschreibung des UI-Teils Anwendungen (axml- und xib- bzw. Storyboard-Dateien).
Allein diese Tatsache führt uns zu der Tatsache, dass für diesen Ansatz zwei Entwicklungsteams gebildet werden müssen. Darüber hinaus müssen Sie die Logik für jede der Plattformen duplizieren: Interaktion mit API und Geschäftslogik.
Was aber, wenn die Anzahl der verwendeten APIs zunimmt?
Dies wirft die Frage auf: Wie kann der Personalbedarf reduziert werden? Beseitigen Sie die Notwendigkeit, Code für jede Plattform zu duplizieren. Es gibt eine ausreichende Anzahl von Frameworks und Technologien, die dieses Problem lösen.
Die Verwendung eines plattformübergreifenden Frameworks (z. B. Xamarin.Forms) ermöglicht es, Code in einer Programmiersprache zu schreiben und Datenlogik und UI-Logik einmal an einer Stelle zu beschreiben. Daher entfällt die Notwendigkeit, zwei Entwicklungsteams einzusetzen. Durch das Kompilieren des Projekts erhalten wir zwei native Anwendungen am Ausgang. Und dies ist der zweite Ansatz.
Ich denke, viele wissen, was Xamarin ist, oder haben zumindest davon gehört, aber wie funktioniert es? Xamarin basiert auf der Open-Source-Implementierung der .NET-Plattform - Mono. Mono enthält einen eigenen C # -Compiler, eine Laufzeit sowie eine Reihe von Bibliotheken, einschließlich der Implementierung von WinForms und ASP.Net.
Ziel des Projekts ist es, in C # geschriebene Programme auf anderen Betriebssystemen als Windows auszuführen - Unix-Systemen, Mac OS und anderen. Das Xamarin-Framework selbst ist im Wesentlichen eine Klassenbibliothek, mit der Entwickler auf das SDK und die Compiler der Plattform zugreifen können. Mit Xamarin.Forms können Sie nicht nur für beide Plattformen in derselben Sprache schreiben, sondern auch Bildschirme mit XAML-Markup entwerfen, das denjenigen vertraut ist, die bereits Erfahrung mit WPF-Anwendungen haben. Als Ergebnis der Projektzusammenstellung erhalten wir auf allen Plattformen ein nahezu identisches Aussehen, da in der Kompilierungsphase alle XF-Steuerelemente für jede Plattform in native konvertiert werden.

Der Entwickler muss nur dann Code für jede Plattform schreiben, wenn Zugriff auf Plattformfunktionen erforderlich ist (z. B. ein Fingerabdruckscanner oder ein Akkuladestand) oder das Steuerungsverhalten optimiert werden muss. In einigen Fällen kann es bei der Entwicklung einer Anwendung erforderlich sein, plattformabhängigen Code zu schreiben, aber selbst in diesem Fall verbietet niemand, in Zukunft Plattformfunktionen auf die Schnittstelle zu übertragen und mit ihr aus dem allgemeinen Projekt zu interagieren.
Eine Programmiersprache, wenig Code und so weiter. Es klingt alles schön, aber Xamarin.Forms ist keine Silberkugel, und all seine Schönheit bricht in Realitätssteine ein. Sobald eine Situation auftritt, in der die eingebauten XF-Steuerungen die Anforderungen für sie nicht mehr erfüllen, wird die Struktur von Bildschirmen und Steuerungen immer komplizierter. Um eine komfortable Arbeit mit Bildschirmen aus einem gemeinsamen Projekt zu gewährleisten, müssen Sie immer mehr benutzerdefinierte Renderings schreiben.
Dies wird zum dritten Ansatz übergehen, den wir bei der Entwicklung von Anwendungen verwenden.
Wir haben bereits herausgefunden, dass die Verwendung von Xamarin Forms die Arbeit erschweren und nicht vereinfachen kann. Um architektonisch komplexe Bildschirme, Designelemente und Steuerelemente zu implementieren, die sich grundlegend von nativen unterscheiden, wurden daher ein Kompromiss und die Möglichkeit gefunden, den ersten und den zweiten Ansatz zu kombinieren.
Wir haben alle die gleichen drei Projekte: ein gemeinsames PCL-Projekt, jedoch ohne Xamarin Forms, und zwei Xamarin Android- und Xamarin iOS-Projekte. Es besteht immer noch die Möglichkeit, alles in einer Sprache zu schreiben, eine gemeinsame Logik zwischen zwei Projekten, aber es gibt keine Einschränkungen für ein einzelnes XAML-Markup. Die UI-Komponente wird von jeder Plattform gesteuert und verwendet native Tools für Android-native AXML- und iOS-XIB-Dateien. Jede Plattform kann ihre Richtlinien einhalten, da die Verbindung zwischen Kern- und Plattformprojekten nur auf Datenebene organisiert wird.
Um eine solche Beziehung zu organisieren, können Sie das MVVM-Entwurfsmuster und seine recht beliebte Implementierung für Xamarin - MVVMCross verwenden. Durch seine Verwendung können Sie für jeden Bildschirm ein gemeinsames ViewModel beibehalten, das die gesamte "Geschäftslogik" der Arbeit beschreibt, und das Rendering der Plattform anvertrauen. Außerdem können zwei Entwickler mit demselben Bildschirm arbeiten (einer mit Logik - der andere mit Benutzeroberfläche) und sich nicht gegenseitig stören. Neben der Implementierung des Musters erhalten wir eine ausreichende Anzahl von Werkzeugen für die Arbeit: die Implementierung von DI und IoC. Um die Interaktion mit der Plattform auf das Niveau von allgemeinem Code zu heben, muss ein Entwickler lediglich eine Schnittstelle deklarieren und auf der Plattform implementieren. Für typische Dinge bietet MvvmCross bereits eine Reihe eigener Plugins an. Im Team verwenden wir das Messenger-Plugin zum Austausch von Nachrichten zwischen der Plattform und dem allgemeinen Code und das Plugin zum Arbeiten mit Dateien (Auswählen von Bildern aus der Galerie usw.).
Wir lösen die Probleme des komplexen Designs und der mehrstufigen Navigation
Wie bereits erwähnt, kann das Framework bei Verwendung komplexer Darstellungen auf dem Bildschirm das Leben mehr verkomplizieren als erleichtern. Aber was nennt man ein komplexes Element? Da ich hauptsächlich in der iOS-Entwicklung tätig bin, wird ein Beispiel für diese Plattform betrachtet. Zum Beispiel kann eine so triviale Sache wie ein Eingabefeld mehrere Zustände und genügend Logik zum Umschalten und Visualisieren haben.
Im Zuge der Arbeit mit Benutzereingaben wurde hier eine solche Eingabesteuerung entwickelt. Er kann seinen Namen über das Eingabefeld heben, mit Masken arbeiten, Präfixe und Postfixes festlegen, benachrichtigen, wenn CapsLock gedrückt wird, Informationen in zwei Modi validieren: Verbot der Eingabe und Ausgabe von Fehlerinformationen. Die Logik innerhalb der Steuerung benötigt ungefähr 1000 Zeilen. Und es scheint: Was kann bei der Gestaltung des Eingabefeldes kompliziert sein?
Ein einfaches Beispiel für eine komplexe Steuerung, die wir gesehen haben. Was ist mit den Bildschirmen?
Zunächst möchte ich klarstellen, dass in den meisten Fällen ein Anwendungsbildschirm eine Klasse ist - UIViewController, der sein Verhalten beschreibt. Während der Entwicklung war die Erstellung einer mehrstufigen Navigation erforderlich. Das Konzept der entwickelten Anwendung besteht darin, Ihre Immobilien zu verwalten und mit Nachbarn und kommunalen Organisationen zu interagieren. Daher wurden drei Navigationsebenen erstellt: Eigenschaft, Präsentationsebene (Heimat, Stadt, Region) und Art des Inhalts. Alle Umschaltungen erfolgen auf einem Bildschirm.
Dies wurde getan, damit der Benutzer, wo immer er sich befindet, versteht, welche Art von Inhalten er sieht. Um eine solche Navigation zu organisieren, besteht der Hauptbildschirm der Anwendung nicht nur aus einem Controller. Optisch kann es in 3 Teile unterteilt werden, aber kann jemand versuchen zu erraten, wie viele Controller hier verwendet werden?
Fünfzehn Hauptsteuerungen. Und das ist nur für den Inhalt.
Hier lebt so ein Monster auf dem Hauptbildschirm und fühlt sich ziemlich gut an. Fünfzehn Controller für einen Bildschirm sind natürlich viel. Dies wirkt sich auf die Geschwindigkeit der gesamten Anwendung aus, und Sie müssen sie irgendwie optimieren.
Wir haben die synchrone Initialisierung abgelehnt: Alle Ansichtsmodelle werden im Hintergrund und nur bei Bedarf initialisiert. Um die Renderzeit zu verkürzen, haben wir auch xib-Dateien für diese Bildschirme aufgegeben: Absolute Positionierung und Mathematik sind immer schneller als die Berechnung der Abhängigkeiten zwischen Elementen.
Um so viele Controller im Auge zu behalten, müssen Sie Folgendes verstehen:
- In welchem Zustand ist jeder von ihnen;
- Wo ist der Benutzer?
- Was er erwartet, wenn er zu einem anderen Controller wechselt.
Zu diesem Zweck habe ich einen separaten Navigationsprozessor geschrieben, der Informationen über den Standort des Benutzers, die Art des angezeigten Inhalts, den Navigationsverlauf usw. speichert. Er kontrolliert die Reihenfolge und Notwendigkeit der Initialisierung.
Da jede Registerkarte ein Controller-Schieberegler ist (um einen Swipe-Übergang für sie zu erstellen), müssen Sie Folgendes verstehen: Jede Registerkarte kann sich in einem eigenen Status befinden (z. B. ist "News" auf der einen Seite geöffnet und "Voting" auf der anderen Seite). Darauf folgt der gleiche Navigationsprozessor. Selbst wenn wir den Präsentationsgrad von zu Hause in die Region ändern, bleiben wir bei der gleichen Art von Inhalten.
Wir steuern den Datenfluss in Echtzeit
Wenn Sie mit so vielen Daten in der Anwendung arbeiten, müssen Sie die Bereitstellung relevanter Informationen in allen Abschnitten in Echtzeit organisieren. Um dieses Problem zu lösen, können 3 Methoden unterschieden werden:
- Zugriff auf die API über Timer oder Trigger und erneutes Anfordern relevanter Inhalte auf den Bildschirmen;
- Stellen Sie eine dauerhafte Verbindung zum Server her und erhalten Sie Änderungen in Echtzeit.
- Erhalten Sie Push mit Inhaltsänderungen.
Jeder Ansatz hat seine Vor- und Nachteile. Daher ist es besser, alle drei zu verwenden und nur die Stärken der einzelnen Ansätze auszuwählen. Wir haben den Inhalt der Anwendung bedingt in verschiedene Typen unterteilt: Hot, Regular und Service. Dies erfolgt, um die akzeptable Zeit zwischen dem Ereignis und der Benachrichtigung des Benutzers zu bestimmen. Zum Beispiel möchten wir eine Chat-Nachricht sofort sehen, nachdem sie an uns gesendet wurde - dies ist heißer Inhalt. Eine weitere Option: Umfrage von Nachbarn. Es macht keinen Unterschied, wann wir ihn jetzt oder in einer Minute sehen, denn dies ist gewöhnlicher Inhalt. Kleine Benachrichtigungen innerhalb der Anwendung (ungelesene Nachrichten, Befehle usw.) sind Dienstinhalte, die dringend zugestellt werden müssen, aber nicht viele Daten aufnehmen.
Es stellt sich heraus:
- Hot Content - permanente Verbindung mit der API;
- Normaler Inhalt - http-Anfragen an die API;
- Systeminhalt - Push-Benachrichtigungen.
Das Interessanteste ist die Aufrechterhaltung einer ständigen Verbindung. Das Schreiben eines eigenen Clients für die Arbeit mit Web-Sockets ist ein Schritt in das Kaninchenloch, daher müssen Sie nach anderen Lösungen suchen. Infolgedessen haben wir bei der SignalR-Bibliothek angehalten. Mal sehen, was es ist.
ASP.Net SignalR ist eine Bibliothek von Microsoft, die die Client-Server-Interaktion in Echtzeit vereinfacht und eine bidirektionale Kommunikation zwischen Client und Server ermöglicht. Der Server enthält eine vollwertige API zum Verwalten der Verbindung, Verbindungsabbruchereignisse, einen Mechanismus zum Kombinieren verbundener Clients zu Gruppen und die Autorisierung.
SignalR kann Websockets, LongPolling und http-Anforderungen als Transport verwenden. Sie können die Art des Transports zwangsweise angeben oder der Bibliothek vertrauen: Wenn Websocket verwendet werden kann, funktioniert es über Websocket. Wenn dies nicht möglich ist, wird es heruntergefahren, bis ein akzeptabler Transport gefunden wird. Diese Tatsache erwies sich als sehr praktisch, da geplant ist, sie auf Mobilgeräten zu verwenden.
Insgesamt, welchen Nutzen bekommen wir:
- Möglichkeit zum Austausch von Nachrichten jeglicher Art zwischen Client und Server;
- Der Mechanismus zum automatischen Umschalten zwischen Web-Sockets, Long Pooling und HTTP-Anforderungen.
- Informationen zum aktuellen Status der Verbindung;
- Eine Gelegenheit, Kunden in Gruppen zu vereinen;
- Praktische Methoden zur Manipulation der Logik des Sendens von Nachrichten in einer Gruppe;
- Die Möglichkeit, den Server zu skalieren.
Dies befriedigt natürlich nicht alle Bedürfnisse, erleichtert aber das Leben spürbar.
Innerhalb des Projekts wird ein Wrapper für die SignalR-Bibliothek verwendet, was die Arbeit damit weiter vereinfacht, nämlich:
- Überwacht den Status der Verbindung, stellt die Verbindung gemäß den angegebenen Bedingungen und im Falle einer Unterbrechung wieder her.
- Es ist möglich, eine Verbindung schnell zu ersetzen oder wieder zu öffnen, die alte asynchron zu beenden und sie dem Garbage Collector zum Zerreißen zu übergeben. Wie sich herausstellte, funktioniert die Verbindungsaufbau-Methode zehnmal schneller als die Schließmethode (Entsorgen oder Stoppen). Dies ist die einzige Möglichkeit, sie zu schließen.
- Organisiert eine Warteschlange zum Senden von Nachrichten, damit das erneute Verbinden oder erneute Öffnen der Verbindung das Senden nicht unterbricht.
- Überträgt die Kontrolle bei unvorhergesehenen Fehlern an die entsprechenden Delegierten.
Jeder dieser Wrapper (wir nennen sie Clients) arbeitet mit dem Caching-System zusammen und kann im Falle einer Unterbrechung nur die Daten anfordern, die er während dieser Zeit möglicherweise übersehen hat. "Jeder", weil mehrere Wirkstoffe gleichzeitig gehalten werden. In der Anwendung befindet sich ein vollwertiger Messenger, der von einem separaten Client bedient wird.
Der zweite Kunde ist für den Empfang von Benachrichtigungen verantwortlich. Wie ich bereits sagte, wird die übliche Art von Inhalten über http-Anfragen erhalten. In Zukunft liegt die Aktualisierung bei diesem Client, der alle wichtigen Änderungen darin meldet (z. B. wurde die Abstimmung von einem Status in einen anderen übertragen, Veröffentlichung neuer Nachrichten).
Visualisieren Sie die Daten in der Anwendung
Daten zu erhalten ist eine Sache, zu zeigen eine andere. Die Aktualisierung von Echtzeitdaten hat ihre eigenen Schwierigkeiten. Sie müssen mindestens entscheiden, wie diese Updates dem Benutzer angezeigt werden sollen. In der Anwendung verwenden wir drei Arten von Benachrichtigungen:
- Benachrichtigung über ungelesene Inhalte;
- Automatische Aktualisierung der Daten auf dem Bildschirm;
- Inhaltsangebot.
Die bekannteste und üblichste Methode, um zu zeigen, dass irgendwo neue Inhalte vorhanden sind, besteht darin, das Abschnittssymbol hervorzuheben. Somit können fast alle Symbole den ungelesenen Inhaltsbenachrichtiger als roten Punkt anzeigen. Interessanter sind Dinge mit automatischen Updates.
Das automatische Aktualisieren von Daten ist nur möglich, wenn neue Inhalte den Bildschirm nicht neu anordnen und die Größe der Steuerelemente nicht ändern. Beispiel: Auf dem Umfragebildschirm: Informationen zu den Stimmen ändern nur den Wert des Fortschrittsbalkens und die Prozentsätze. Solche Änderungen erfordern keine Größenänderung und können sofort und problemlos angewendet werden.
Schwierigkeiten treten auf, wenn Sie Listen neuen Inhalt hinzufügen müssen. Alle Listen in der Anwendung sind ScrollView und weisen mehrere Merkmale auf: Fenstergröße, Inhaltsgröße und Bildlaufposition. Alle haben einen statischen Anfang (oben auf dem Bildschirm mit den Koordinaten 0; 0) und können nach unten erweitert werden. Hinzufügen neuer Inhalte in der Liste, am Ende keine Probleme, die Liste wird dauern. Aber neue Inhalte sollten oben erscheinen, und das ist das Bild:

Wenn wir 3 Elemente haben, sind wir 2 - die Schriftrolle springt hoch. Und da ständig neue Inhalte eintreffen können, kann der Benutzer nicht normal scrollen. Sie könnten sagen: Warum nicht die Größe des neuen Inhalts berechnen und den Bildlauf nach unten auf diesen Wert verschieben? Ja, das kann man machen. Dann müssen Sie jedoch die Bildlaufposition manuell steuern. Wenn der Benutzer in diesem Moment in eine beliebige Richtung blättert, wird seine Aktion unterbrochen. Aus diesem Grund können solche Bildschirme ohne Zustimmung des Benutzers nicht in Echtzeit aktualisiert werden.
Die beste Lösung in dieser Situation wäre, den Benutzer darüber zu informieren, dass beim Scrollen des Feeds jemand neue Inhalte gepostet hat. In unserem Design sieht es aus wie ein roter Kreis in der Ecke des Bildschirms. Durch Klicken darauf gibt der Benutzer seine bedingte Zustimmung, dass wir es wieder oben auf dem Bildschirm anzeigen und frischen Inhalt anzeigen.
Mit diesem Ansatz haben wir natürlich die Probleme des „Ausrutschens“ des Inhalts vermieden, aber sie mussten noch gelöst werden. Auf dem Chat-Bildschirm müssen nämlich während der Kommunikation und Interaktion mit dem Bildschirm neue Inhalte an verschiedenen Stellen angezeigt werden.
Der Unterschied zwischen Chat- und regulären Listen besteht darin, dass sich am unteren Bildschirmrand neuer Inhalt befindet. Da dies ein „Schwanz“ ist, können Sie dort problemlos Inhalte hinzufügen. Der Benutzer verbringt 90% der Zeit hier, was bedeutet, dass Sie die Bildlaufposition beim Empfangen und Senden von Nachrichten ständig beibehalten und nach unten verschieben müssen. In einem Live-Gespräch müssen solche Aktionen ziemlich oft durchgeführt werden.
Der zweite Punkt: Laden des Verlaufs beim Scrollen nach oben. Gerade beim Laden der Story befinden wir uns in einer Situation, in der es notwendig ist, Nachrichten über der Überprüfungsstufe zu platzieren (was zu Verzerrungen führt), damit die Schriftrolle glatt und kontinuierlich ist. Und wie wir bereits wissen, ist es unmöglich, die Bildlaufposition manuell zu steuern, um den Benutzer nicht zu stören.
Und wir haben eine Lösung gefunden: Wir haben sie umgedreht. Der Bildschirmwechsel löste zwei Probleme gleichzeitig:
- Das Ende der Liste befindet sich oben, sodass wir nahtlos eine Story hinzufügen können, ohne die Benutzerrolle zu beeinträchtigen.
- Die letzte Nachricht steht immer ganz oben auf der Liste und wir müssen den Bildschirm vorher nicht scrollen.
Diese Lösung trug auch dazu bei, das Rendern zu beschleunigen und unnötige Vorgänge mit der Bildlaufsteuerung zu vermeiden.
Apropos Leistung. In den ersten Versionen des Bildschirms wurden beim Scrollen von Nachrichten merkliche Nachteile festgestellt.
Da der Inhalt des "Geldes" bunt ist - Text, Dateien, Fotos - müssen Sie die Größe der Zelle ständig neu berechnen, Elemente im Geld hinzufügen und entfernen. Daher war eine Optimierung der Blase erforderlich. Wir haben das gleiche wie beim Hauptbildschirm gemacht und den Teig teilweise mit absoluter Positionierung gerendert.Wenn Sie mit Listen in iOS arbeiten, müssen Sie vor dem Zeichnen einer Zelle deren Höhe kennen. Bevor Sie der Liste eine neue Nachricht hinzufügen, müssen Sie daher alle erforderlichen Informationen für die Anzeige in einem separaten Stream vorbereiten, die Höhe der Zellen berechnen, die Benutzerdaten verarbeiten und die Zelle erst dann zur Liste hinzufügen, wenn wir alles Notwendige herausgefunden und zwischengespeichert haben.Als Ergebnis erhalten wir einen reibungslosen Bildlauf und keinen überladenen UI-Stream.Zusammenfassend:
- Die plattformübergreifende Entwicklung spart Zeit und Geld.
- , , ;
- , ;
- ;
- SignalR – - ;
- ;
- , , ;
- , SignalR-, , , , .