Gedankenexperiment: Flattere weiter

    Vor kurzem entdeckte ich Flutter - ein neues Framework von Google für die Entwicklung plattformübergreifender mobiler Anwendungen - und hatte sogar die Möglichkeit, einer Person, die noch nie programmiert wurde, die Grundlagen von Flutter zu zeigen. Flutter selbst ist in Dart geschrieben, einer Sprache, die im Chrome-Browser geboren wurde und in die Welt der Konsole entkam - und das brachte mich dazu, zu denken, "hmm, aber Flutter hätte gut auf Go geschrieben werden können."


    Warum nicht Sowohl Go als auch Dart wurden von Google erstellt, beide sind kompilierte Sprachen. Einige Events sind etwas anders. Go wäre ein großartiger Kandidat für ein großes Projekt wie Flutter. Jemand wird sagen - es gibt keine Klassen, Generika und Ausnahmen in Go, also passt es nicht.


    Stellen wir uns vor, Flutter ist bereits auf Go geschrieben. Wie wird der Code aussehen und wird er im Allgemeinen funktionieren?



    Was ist los mit Dart?


    Ich habe diese Sprache seit ihrer Einführung als Alternative zu JavaScript in Browsern verfolgt. Dart war eine Zeitlang in den Chrome-Browser integriert, und die Hoffnung bestand darin, JS zu erzwingen. Es war äußerst traurig, im März 2015 zu lesen, dass die Unterstützung für Dart von Chrome entfernt wurde .


    Dart selbst ist großartig! Im Grunde ist nach JavaScript jede Sprache großartig, aber nach, sagen wir, Go, Dart ist nicht so schön. aber ganz ok Es verfügt über alle erdenklichen und unvorstellbaren Features - Klassen, Generics, Exceptions, Futures, Async-Waitit, Event-Loop, JIT / AOT, Garbage Collection, Funktionsüberladung - nennen Sie jedes bekannte Feature aus der Theorie der Programmiersprachen und in Dart wird es einen hohen Anteil haben Wahrscheinlichkeiten Dart hat eine spezielle Syntax für fast jedes Stück - eine spezielle Syntax für Getter / Setter, eine spezielle Syntax für abgekürzte Konstruktoren, eine spezielle Syntax für spezielle Syntax und vieles mehr.


    Dies macht Dart auf einen Blick den Leuten bekannt, die bereits in einer anderen Programmiersprache programmiert haben, und das ist großartig. Bei dem Versuch, all diese Fülle von Besonderheiten in einem unprätentiösen "Hallo, Welt" -Beispiel zu erklären, stellte ich fest, dass es im Gegenteil schwierig zu meistern ist.


    • Alle "Besonderheiten" der Sprache wurden durcheinander gebracht - "eine spezielle Methode namens" Konstruktor "", "eine spezielle Syntax für die automatische Initialisierung", "eine spezielle Syntax für benannte Parameter" usw.
    • alle "versteckten" verwirrten - "aus welchem ​​import ist diese Funktion? Ist sie versteckt, schaut man den Code an, den man nicht herausfinden kann?", "Warum gibt es einen Konstruktor in dieser Klasse, aber gibt es keinen? Es gibt einen, aber er ist versteckt" und so weiter
    • alle "mehrdeutig" verwirrt - "also hier Funktionsparameter mit oder ohne Namen erstellen?", "sollte es const oder final geben?", "verwenden Sie hier normale Funktionssyntax oder" abgekürzt mit Pfeil "und so weiter.

    Im Prinzip erfasst diese Dreieinigkeit - "speziell", "versteckt" und "mehrdeutig" - die Essenz dessen, was die Leute in Programmiersprachen "Magie" nennen. Dies sind Funktionen, die das Schreiben von Code vereinfachen, jedoch das Lesen und Verstehen erschweren.


    Und genau hier nimmt Go eine andere Position ein als andere Sprachen und verteidigt sich mit aller Kraft. Go ist eine Sprache, die fast keine Zauberei besitzt - die Menge an "versteckt", "speziell" und "mehrdeutig" wird auf ein Minimum reduziert. Aber Go hat seine Nachteile.


    Was ist los mit Go?


    Da wir über Flutter sprechen und dies ein UI-Framework ist, betrachten wir Go als ein Werkzeug zum Beschreiben und Arbeiten mit der Benutzeroberfläche. Im Allgemeinen sind UI-Frameworks eine enorme Aufgabe und erfordern fast immer spezielle Lösungen. Einer der gebräuchlichsten Ansätze in der Benutzeroberfläche ist die Erstellung von DSL - domänenspezifischen Sprachen -, die in Form von Bibliotheken oder Frameworks implementiert werden, die speziell auf die Anforderungen der Benutzeroberfläche zugeschnitten sind. Und meistens hört man die Meinung, dass Go objektiv eine schlechte Sprache für DSL ist.


    Im Wesentlichen bedeutet DSL, eine neue Sprache zu erstellen - Begriffe und Verben -, die ein Entwickler bearbeiten kann. Der Code sollte die Hauptmerkmale der grafischen Benutzeroberfläche und ihrer Komponenten klar beschreiben, flexibel genug sein, um die Vorstellungskraft des Designers freizusetzen, und dennoch hart genug sein, um sie gemäß bestimmten Regeln einzuschränken. Sie sollten beispielsweise in der Lage sein, die Schaltflächen in einem Container zu platzieren und das Symbol an der richtigen Stelle in dieser Schaltfläche zu platzieren. Der Compiler sollte jedoch einen Fehler zurückgeben, wenn Sie versuchen, eine Schaltfläche beispielsweise in Text einzufügen.


    Außerdem sind die Sprachen zur Beschreibung der Benutzeroberfläche oft deklarativ. Sie geben die Möglichkeit, die Benutzeroberfläche in Form von "was ich gerne sehen möchte" zu beschreiben und dem Framework zu zeigen, welchen Code und wie er ausgeführt wird.


    Einige Sprachen wurden ursprünglich mit solchen Aufgaben an den Sehenswürdigkeiten entwickelt, nicht aber Go. Es scheint, dass das Schreiben von Flutter on Go die einzige Aufgabe ist!


    Ode flattert


    Wenn Sie Flutter noch nicht kennen, empfehle ich Ihnen dringend, das nächste Wochenende damit zu verbringen, Schulungsvideos anzusehen oder Tutorials zu lesen, die viele sind. Weil Flutter zweifellos die Spielregeln bei der Entwicklung von mobilen Anwendungen aufhebt. Und wahrscheinlich nicht nur mobil: Es gibt bereits Renderer (Flutter-Begriffe, Embedder), um Flutter-Anwendungen als native Dekstop-Anwendungen und als Webanwendungen auszuführen .


    Es ist leicht zu erlernen, es ist logisch, es verfügt über eine riesige Bibliothek mit schönen Widgets zu Material Design (und nicht nur), es hat eine großartige Community und eine hervorragende Abstimmung (wenn Sie die einfache Arbeit mit go build/run/testGo mögen , wird Flutter eine ähnliche Erfahrung machen).


    Vor einem Jahr musste ich eine kleine mobile Anwendung schreiben (natürlich für iOS und Android), und ich wusste, dass die Komplexität der Entwicklung einer qualitativ hochwertigen Anwendung für beide Plattformen zu groß war (die Anwendung war nicht die Hauptaufgabe) - ich musste auslagern und dafür bezahlen. Das Schreiben einer einfachen, aber qualitativ hochwertigen Anwendung, die auf allen Geräten ausgeführt wird, war selbst für eine Person mit fast 20 Jahren Programmiererfahrung eine unmögliche Aufgabe. Und es war immer Quatsch für mich.


    Mit Flutter habe ich diese Anwendung für 15 Uhr neu geschrieben, während ich das Framework selbst von Grund auf lernte. Wenn mir jemand gesagt hätte, dass dies etwas früher sein könnte, hätte ich es nicht geglaubt.


    Das letzte Mal, als ich mit der Entdeckung einer neuen Technologie einen ähnlichen Produktivitätsanstieg sah, war vor fünf Jahren, als ich Go entdeckte. Dieser Moment hat mein Leben verändert.


    Ich empfehle daher, Flutter zu lernen, und dieses Tutorial ist sehr gut .


    "Hallo Welt" beim Flattern


    Wenn Sie eine neue Anwendung erstellen flutter create, erhalten Sie ein solches Programm mit einem Titel, Text, einem Zähler und einer Schaltfläche, die den Zähler erhöht.



    Ich denke, das ist ein großartiges Beispiel. um es auf unser imaginäres Flutter on Go zu schreiben. Es hat fast alle grundlegenden Konzepte des Frameworks, wo Sie die Idee testen können. Schauen wir uns den Code an (dies ist eine Datei):


    import 'package:flutter/material.dart';
    void main() => runApp(MyApp());
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
      final String title;
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
      void _incrementCounter() {
        setState(() {
          _counter++;
        });
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.display1,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
        );
      }
    }

    Sehen wir uns den Code in Teilen an, analysieren, was und wie er von Go kommt, und schauen wir uns die verschiedenen Optionen an, die wir haben.


    Wir übersetzen den Code in Go


    Der Start wird einfach und unkompliziert sein - Abhängigkeiten und Startfunktionen importieren main(). Nichts kompliziertes oder interessantes hier, die Änderung ist fast syntaktisch:


    package hello
    import"github.com/flutter/flutter"funcmain() {
        app := NewApp()
        flutter.Run(app)
    }

    Der einzige Unterschied besteht darin, dass anstelle des Startens MyApp()- einer Funktion, die ein Konstruktor ist - einer speziellen Funktion, die in einer Klasse namens MyApp versteckt ist - wir die einfache explizite und nicht versteckte Funktion nennen NewApp(). Es tut dasselbe, aber es ist viel klarer zu erklären und zu verstehen, was es ist, wie es beginnt und wie es funktioniert.


    Widget-Klassen


    In Flutter besteht alles aus Widgets. In der Dart-Version von Flutter wird jedes Widget als Klasse implementiert, die spezielle Klassen für Widgets von Flutter erbt.


    In Go gibt es keine Klassen und dementsprechend Klassenhierarchien, da die Welt nicht objektorientiert und schon gar nicht hierarchisch ist. Für Programmierer, die nur mit einem klassenorientierten OOP-Modell vertraut sind, kann dies eine Offenbarung sein, ist es aber wirklich nicht. Die Welt ist ein riesiger interlaced Graph von Konzepten, Prozessen und Interaktionen. Es ist nicht perfekt strukturiert, aber nicht chaotisch, und der Versuch, es in Klassenhierarchien zu quetschen, ist der zuverlässigste Weg, um die Codebasis unlesbar und umständlich zu machen - genau wie die meisten Codebasen derzeit.



    Ich schätze wirklich gehen für das, was seine Schöpfer gearbeitet haben , dieses allgegenwärtige Konzept der Klassen zu überdenken und in Go implementiert ist viel einfacher und leistungsfähiges Konzept der PLO, die nicht zufällig, näher zu sein , stellte sich heraus , was der Schöpfer der PLO, Alan Kay, im Sinn hatte .


    In Go repräsentieren wir jede Abstraktion in Form einer bestimmten Typstruktur:


    type MyApp struct {
        // ...
    }

    In der Dart-Version von Flutter MyAppmuss StatelessWidgetdie Methode erben und überschreiben build. Dies ist erforderlich, um zwei Probleme zu lösen:


    1. Geben Sie unserem Widget ( MyApp) einige spezielle Eigenschaften / Methoden
    2. Erlauben Sie Flutter, unseren Code im Build / Render-Prozess aufzurufen

    Ich kenne Flutter-Interna nicht, also sagen wir, Punkt Nummer 1 steht nicht in Frage, und wir müssen es nur tun. In Go gibt es eine einzige und offensichtliche Lösung für dieses Problem - Einbettungstypen :


    type MyApp struct {
        flutter.Core
        // ...
    }

    Dieser Code fügt alle Eigenschaften und Methoden flutter.Corezu unserem Typ hinzu MyApp. Ich nannte ihn Corestatt Widget, denn erstens ist die Art der Einbettung nicht unser machen MyAppWidget, und zum anderen, dass der Name sehr erfolgreich im Rahmen verwendet wird, GopherJS Vecty (so etwas wie die Reaktion, nur zum Ziel ). Ich werde etwas später auf das Thema der Ähnlichkeit Vecty und Flutter eingehen.


    Der zweite Punkt - die Implementierung der Methode build(), die Flutter verwenden kann - wird auch in Go einfach und eindeutig gelöst. Wir müssen nur eine Methode mit einer bestimmten Signatur hinzufügen, die eine bestimmte Schnittstelle erfüllt, die irgendwo in der Bibliothek unseres fiktiven Flutters on Go definiert ist:


    flutter.go:


    type Widget interface {
        Build(ctx BuildContext) Widget
    }

    Und jetzt unser main.go:


    type MyApp struct {
        flutter.Core
        // ...
    }
    // Build renders the MyApp widget. Implements Widget interface.func(m *MyApp)Build(ctx flutter.BuildContext)flutter.Widget {
        return flutter.MaterialApp()
    }

    Wir können hier einige Unterschiede feststellen:


    • Code ein wenig ausführlich -  BuildContext, Widgetund MaterialAppauf dem Import angezeigt fluttervor ihnen.
    • Der Code ist etwas weniger unbegründet - es gibt keine Wörter wie extends Widgetoder@override
    • Die Methode Build()beginnt mit einem Großbuchstaben, da sie die Publizität der Methode in Go bedeutet. In Dart wird die Publizität dadurch bestimmt, ob der Name mit einem Unterstrich (_) beginnt oder nicht.

    Um ein Widget in Flutter on Go zu flutter.Coreerstellen , müssen wir den Typ einbetten und die Schnittstelle implementieren flutter.Widget. Nachdem dies aussortiert wurde, graben Sie weiter.


    Zustand


    Das war eines der Dinge, die mich bei Flutter sehr verwirrt haben. Es gibt zwei verschiedene Klassen -  StatelessWidgetund StatefulWidget. Für mich ist ein "stateless-Widget" dasselbe Widget, nur ohne, hmm, Daten, Status. Warum gibt es hier eine neue Klasse? Okay, damit kann ich leben.


    Darüber hinaus können Sie nicht nur eine andere Klasse ( StatefulWidget) erben , sondern Sie müssen diese Art von Magie schreiben (IDE wird es für Sie tun, aber nicht das Wesentliche):


    class MyHomePage extends StatefulWidget {
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
      void _incrementCounter() {
        setState(() {
          _counter++;
        });
      }
      @override
      Widget build(BuildContext context) {
          return Scaffold()
      }
    }

    Hmm, mal sehen was hier passiert.


    Grundsätzlich ist die Aufgabe wie folgt: Fügen Sie dem Widget (state) einen Status hinzu (in unserem Fall einen Zähler), und teilen Sie der Flutter-Engine mit, wann wir den Status geändert haben, um das Widget neu zu zeichnen. Dies ist die tatsächliche Komplexität des Problems (wesentliche Komplexität in Bezug auf Brooks).


    Der Rest ist zusätzliche Komplexität (zufällige Komplexität). Flutter on Dart wartet mit einer neuen Klasse auf State, die Generics verwendet und ein Widget als Typparameter akzeptiert. Außerdem wird eine Klasse erstellt _MyHomePageState, die erbt State виджета MyApp... ok, es ist immer noch möglich, sie irgendwie zu verdauen. Aber warum wird die Methode build()von der State-Klasse definiert und nicht von der Klasse, welches Widget? Brrr ....


    Die Antwort auf diese Frage finden Sie im Flutter-FAQ. Sie wird hier ausführlich beschrieben und eine kurze Antwort - um eine bestimmte Klasse von Fehlern während der Vererbung zu vermeiden StatefulWidget. Mit anderen Worten, dies ist eine Problemumgehung, um das Problem des klassenorientierten OOP-Designs zu lösen. Chic


    Wie würden wir es in Go machen?


    Erstens würde ich persönlich entschieden, keine separate Entität für den "Staat" zu erstellen  State. Immerhin haben wir in jedem spezifischen Typ bereits einen Zustand - dies sind nur die Felder der Struktur. Die Sprache hat uns dieses Wesen sozusagen bereits gegeben. Das Erstellen einer ähnlichen Entität verwirrt nur den Programmierer.


    Die Aufgabe besteht natürlich darin, Flutter die Fähigkeit zu geben, auf eine Zustandsänderung zu reagieren (dies ist schließlich das Wesen reaktiver Programmierung). Und wenn wir den Entwickler "bitten" können, eine spezielle Funktion ( setState()) zu verwenden, können wir im Gegenzug auch eine spezielle Funktion verwenden, um die Engine zu sprechen, wenn es erforderlich ist, neu zu zeichnen und wann nicht. Am Ende müssen nicht alle Zustandsänderungen neu gezeichnet werden, und hier haben wir noch mehr Kontrolle:


    type MyHomePage struct {
        flutter.Core
        counter int
    }
    // Build renders the MyHomePage widget. Implements Widget interface.func(m *MyHomePage)Build(ctx flutter.BuildContext)flutter.Widget {
        return flutter.Scaffold()
    }
    // incrementCounter increments widgets's counter by one.func(m *MyHomePage)incrementCounter() {
        m.counter++
        flutter.Rerender(m)
        // or m.Rerender()// or m.NeedsUpdate()
    }

    Sie können mit verschiedenen Benennungsoptionen spielen - ich mag NeedsUpdate()die Direktheit und die Tatsache, dass dies eine Widget-Eigenschaft ist (abgeleitet von flutter.Core), aber die globale Methode flutter.Rerender()sieht auch gut aus. Es ist zwar falsch, dass das Widget jetzt sofort neu gezeichnet wird. Dies ist jedoch nicht der Fall - es wird beim nächsten Frame-Update neu gezeichnet, und die Häufigkeit des Methodenaufrufs kann viel höher sein als die Zeichnungsfrequenz - aber unsere Flutter-Engine sollte sich damit auseinandersetzen.


    Die Idee ist jedoch, dass wir die notwendige Aufgabe gerade gelöst haben, ohne hinzuzufügen:


    • neuer Typ
    • Generika
    • Sonderregeln für den Lese- / Schreibzustand
    • spezielle neu definierte Methoden

    Außerdem ist die API viel klarer und übersichtlicher - erhöhen Sie einfach den Zähler (wie in jedem anderen Programm) und bitten Sie Flutter, das Widget neu zu zeichnen. Das ist genau das, was nicht sehr offensichtlich ist, wenn wir nur setStateanrufen - was nicht nur eine spezielle Funktion zum Setzen des Zustands ist, sondern eine Funktion, die eine Funktion (wtf?) Zurückgibt, in der wir bereits etwas mit dem Zustand tun. Versteckte Magie in Sprachen und Frameworks macht es wiederum schwer, Code zu verstehen und zu lesen.


    In unserem Fall haben wir das gleiche Problem gelöst, der Code ist einfacher und zweimal kürzer.


    Widgets mit Status in anderen Widgets


    Als logische Fortsetzung des Themas betrachten wir die Verwendung des Status-Widget in einem anderen Widget in Flutter:


    @override
    Widget build(BuildContext context) {
        return MaterialApp(
            title: 'Flutter Demo',
            home: MyHomePage(title: 'Flutter Demo Home Page'),
        );
    }

    MyHomePageHier ist es ein "State Widget" (es hat einen Zähler), und wir erstellen es, indem wir den Konstruktor MyHomePage()während des Builds aufrufen ... Warten Sie, was ist das?


    build()aufgerufen, um das Widget neu zu zeichnen, möglicherweise viele Male pro Sekunde. Warum müssen wir jedes Mal während der Zeichnung ein Widget erstellen, insbesondere mit dem Status? Es macht keinen Sinn.


    Es stellt sich heraus, dass Flutter diese Trennung zwischen Widgetund Stateverwendet, um diese Initialisierung / Zustandsverwaltung vor dem Programmierer zu verbergen (mehr versteckte Dinge, mehr!). Es erstellt jedes Mal ein neues Widget. Wenn es jedoch bereits erstellt wurde, wird es automatisch gefunden und an das Widget angehängt. Diese Magie geschieht unsichtbar und ich habe keine Ahnung, wie sie funktioniert - Sie müssen den Code lesen.


    Ich denke, das ist ein wahres Übel beim Programmieren - sich so gut wie möglich vor dem Programmierer zu verstecken und es mit Ergonomie zu rechtfertigen. Ich bin sicher, dass der durchschnittliche Programmierer den Flattercode nicht lesen wird, um zu verstehen, wie diese Magie funktioniert, und es ist unwahrscheinlich, dass sie verstehen werden, wie und was miteinander verbunden ist.


    Für die Go-Version möchte ich definitiv eine solche versteckte Magie nicht und würde die explizite und sichtbare Initialisierung vorziehen, auch wenn dies einen etwas mehr unbegründeten Code bedeutet. Flutters Dart-Ansatz kann sicherlich implementiert werden, aber ich liebe Go, um die Magie zu minimieren, und ich würde diese Philosophie gerne in Frameworks sehen. Daher würde mein Code für Widgets mit dem Status in der Baumstruktur der Widgets folgendermaßen aussehen:


    // MyApp is our top application widget.type MyApp struct {
        flutter.Core
        homePage *MyHomePage
    }
    // NewMyApp instantiates a new MyApp widgetfuncNewMyApp() *MyApp {
        app := &MyApp{}
        app.homePage = &MyHomePage{}
        return app
    }
    // Build renders the MyApp widget. Implements Widget interface.func(m *MyApp)Build(ctx flutter.BuildContext)flutter.Widget {
        return m.homePage
    }
    // MyHomePage is a home page widget.type MyHomePage struct {
        flutter.Core
        counter int
    }
    // Build renders the MyHomePage widget. Implements Widget interface.func(m *MyHomePage)Build(ctx flutter.BuildContext)flutter.Widget {
        return flutter.Scaffold()
    }
    // incrementCounter increments app's counter by one.func(m *MyHomePage)incrementCounter() {
        m.counter++
        flutter.Rerender(m)
    }

    Dieser Code verliert Versionen auf Dart dadurch, dass ich, wenn ich die homePageWidgets aus dem Baum der Widgets entfernen und durch etwas anderes ersetzen möchte, sie an drei Stellen reinigen muss, anstatt an einer. Im Gegenzug bekommen wir ein vollständiges Bild davon, was, wo und wie passiert, wo Speicher zugewiesen wird, wer wen verursacht und so weiter - der Code in Ihrer Hand ist verständlich und leicht lesbar.


    Übrigens hat Flutter immer noch so etwas wie StatefulBuilder , was noch mehr Magie hinzufügt und es erlaubt, Widgets mit dem Status "on the fly" zu erstellen.


    DSL


    Jetzt nehmen wir den lustigsten Teil. Wie haben wir einen Widget-Baum auf Go? Wir möchten, dass es kurz, sauber, einfach in Refactoring und Änderungen aussieht, die räumlichen Beziehungen zwischen Widgets beschreibt (Widgets, die visuell nahe sind, in der Nähe und in der Beschreibung sein sollten) und gleichzeitig flexibel genug sind, um darin ein beliebiges zu beschreiben Code wie Eventhandler.


    Ich finde die Option für Dart ziemlich schön und eloquent:


    return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text('You have pushed the button this many times:'),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.display1,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
    );

    Jedes Widget verfügt über einen Konstruktor, der optionale Parameter akzeptiert. Was den Eintrag hier wirklich schön macht, sind die benannten Parameter der Funktionen .


    Benannte Parameter


    Wenn Sie mit diesem Begriff nicht vertraut sind, werden die Parameter der Funktion in vielen Sprachen als "positional" bezeichnet, da ihre Position für die Funktion wichtig ist:


    Foo(arg1, arg2, arg3)

    und bei benannten Parametern wird alles durch ihren Namen im Aufruf festgelegt:


    Foo(name: arg1, description: arg2, size: arg3)

    Dies fügt Text hinzu, speichert jedoch Klicks und Bewegungen durch den Code, um zu verstehen, was die Parameter bedeuten.


    Im Fall des Widget-Baums spielen sie eine Schlüsselrolle für die Lesbarkeit. Vergleichen Sie den gleichen Code wie oben, jedoch ohne benannte Parameter:


    return Scaffold(
          AppBar(
              Text(widget.title),
          ),
          Center(
            Column(
              MainAxisAlignment.center,
              <Widget>[
                Text('You have pushed the button this many times:'),
                Text(
                  '$_counter',
                  Theme.of(context).textTheme.display1,
                ),
              ],
            ),
          ),
          FloatingActionButton(
            _incrementCounter,
            'Increment',
            Icon(Icons.add),
          ),
        );

    Nicht das. ist es wahr Es ist nicht nur schwieriger zu verstehen (Sie müssen bedenken, was jeder Parameter bedeutet und was sein Typ ist, und dies ist eine erhebliche kognitive Belastung), er gibt uns auch nicht die Freiheit, zu wählen, welche Parameter wir übertragen möchten. Beispielsweise möchten Sie möglicherweise nicht für Ihre Materialanwendung FloatingActionButton, so dass Sie dies nicht in den Parametern angeben. Ohne benannte Parameter müssen wir entweder alle möglichen Widgets festlegen lassen oder mit Reflexion auf Magie zurückgreifen, um herauszufinden, welche Widgets übertragen wurden.


    Da Go keine Funktionen und benannten Parameter überlädt, ist dies für Go keine leichte Aufgabe.


    Gehe Baum Widgets


    Version 1


    Schauen wir uns das Scaffold- Objekt genauer an , ein praktischer Wrapper für eine mobile Anwendung. Es hat mehrere Eigenschaften - appBar, drawe, home, bottomNavigationBar, floatingActionBar - und dies sind alles Widgets. Wenn Sie einen Widgets-Baum erstellen, müssen Sie dieses Objekt irgendwie initialisieren, indem Sie die oben genannten Widget-Eigenschaften übergeben. Nun, dies unterscheidet sich nicht zu sehr von der üblichen Erstellung und Initialisierung von Objekten.


    Versuchen wir den Ansatz "in der Stirn":


    return flutter.NewScaffold(
        flutter.NewAppBar(
            flutter.Text("Flutter Go app", nil),
        ),
        nil,
        nil,
        flutter.NewCenter(
            flutter.NewColumn(
                flutter.MainAxisCenterAlignment,
                nil,
                []flutter.Widget{
                    flutter.Text("You have pushed the button this many times:", nil),
                    flutter.Text(fmt.Sprintf("%d", m.counter), ctx.Theme.textTheme.display1),
                },
            ),
        ),
        flutter.FloatingActionButton(
            flutter.NewIcon(icons.Add),
            "Increment",
            m.onPressed,
            nil,
            nil,
        ),
    )

    Bestimmt nicht der schönste UI-Code. Das Wort ist flutterüberall und fragt. Um es zu verbergen (eigentlich hätte ich das Paket anrufen sollen material, aber nicht flutter, aber nicht die Essenz), sind die namenlosen Parameter völlig unübersichtlich und diese sind nilüberall eindeutig verwirrend.


    Version 2


    Da der größte Teil des Codes ohnehin einen Typ oder eine Funktion aus dem Paket verwendet flutter, können wir das Format "dot import" verwenden, um das Paket in unseren Namespace zu importieren und somit den Paketnamen auszublenden:


    import . "github.com/flutter/flutter"

    Stattdessen können flutter.Textwir einfach schreiben Text. Dies ist normalerweise eine schlechte Praxis, aber wir arbeiten mit dem Framework, und dieser Import wird in jeder Zeile buchstäblich sein. Aus meiner Praxis ist dies genau der Fall, für den ein solcher Import zulässig ist - zum Beispiel bei der Verwendung eines wunderbaren GoConvey-Testframeworks .


    Mal sehen, wie der Code aussehen wird:


    return NewScaffold(
        NewAppBar(
            Text("Flutter Go app", nil),
        ),
        nil,
        nil,
        NewCenter(
            NewColumn(
                MainAxisCenterAlignment,
                nil,
                []Widget{
                    Text("You have pushed the button this many times:", nil),
                    Text(fmt.Sprintf("%d", m.counter), ctx.Theme.textTheme.display1),
                },
            ),
        ),
        FloatingActionButton(
            NewIcon(icons.Add),
            "Increment",
            m.onPressed,
            nil,
            nil,
        ),
    )

    Schon besser, aber diese Nullen und unbenannten Parameter ....


    Version 3


    Schauen wir uns an, wie der Code aussehen wird, wenn wir Reflection verwenden (die Möglichkeit, den Code während des Programms zu untersuchen), um die übergebenen Parameter zu analysieren. Dieser Ansatz wird in mehreren frühen HTTP-Frameworks auf Go (beispielsweise Martini ) verwendet und gilt als sehr schlechte Praxis. Er ist unsicher, verliert die Bequemlichkeit des Typsystems, ist relativ langsam und fügt dem Code Magie hinzu.


    return NewScaffold(
        NewAppBar(
            Text("Flutter Go app"),
        ),
        NewCenter(
            NewColumn(
                MainAxisCenterAlignment,
                []Widget{
                    Text("You have pushed the button this many times:"),
                    Text(fmt.Sprintf("%d", m.counter), ctx.Theme.textTheme.display1),
                },
            ),
        ),
        FloatingActionButton(
            NewIcon(icons.Add),
            "Increment",
            m.onPressed,
        ),
    )

    Nicht schlecht und es sieht aus wie die ursprüngliche Version von Dart, aber das Fehlen benannter Parameter schmerzt immer noch sehr.


    Version 4


    Lassen Sie uns ein wenig zurückgehen und uns fragen, was genau wir zu tun versuchen. Wir brauchen den Dart-Ansatz nicht blind zu kopieren (obwohl dies ein netter Bonus ist - weniger, um Leute zu unterrichten, die bereits mit Flutter on Dart vertraut sind). Im Wesentlichen erstellen wir einfach neue Objekte und weisen ihnen Eigenschaften zu.


    Vielleicht versuchen Sie es so?


    scaffold := NewScaffold()
    scaffold.AppBar = NewAppBar(Text("Flutter Go app"))
    column := NewColumn()
    column.MainAxisAlignment = MainAxisCenterAlignment
    counterText := Text(fmt.Sprintf("%d", m.counter))
    counterText.Style = ctx.Theme.textTheme.display1
    column.Children = []Widget{
      Text("You have pushed the button this many times:"),
      counterText,
    }
    center := NewCenter()
    center.Child = column
    scaffold.Home = center
    icon := NewIcon(icons.Add),
    fab := NewFloatingActionButton()
    fab.Icon = icon
    fab.Text = "Increment"
    fab.Handler = m.onPressed
    scaffold.FloatingActionButton = fab
    return scaffold

    Ein solcher Ansatz wird funktionieren, und obwohl er unser Problem mit benannten Parametern löst, verwirrt er immer noch das Bild des Widget-Baums als Ganzes. Erstens wird die Reihenfolge, in der Widgets definiert werden, umgekehrt - je tiefer das Widget, desto früher sollte es erstellt werden. Zweitens haben wir die praktische visuelle Struktur des Codes verloren, die auch mit Hilfe von Einrückungen schnell die Tiefe des Widgets im Baum deutlich machte.


    Übrigens wurde dieser Ansatz in UI-Frameworks wie GTK oder Qt sehr lange verwendet . Sehen Sie sich zum Beispiel den Code aus der aktuellen Qt 5- Dokumentation an :


     QGridLayout *layout = new QGridLayout(this);
        layout->addWidget(new QLabel(tr("Object name:")), 0, 0);
        layout->addWidget(m_objectName, 0, 1);
        layout->addWidget(new QLabel(tr("Location:")), 1, 0);
        m_location->setEditable(false);
        m_location->addItem(tr("Top"));
        m_location->addItem(tr("Left"));
        m_location->addItem(tr("Right"));
        m_location->addItem(tr("Bottom"));
        m_location->addItem(tr("Restore"));
        layout->addWidget(m_location, 1, 1);
        QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
        connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
        connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
        layout->addWidget(buttonBox, 2, 0, 1, 2);
    

    Daher gebe ich völlig zu, dass dieses Format für jemanden bekannter und bekannter ist. Es ist jedoch schwer zu bestreiten, dass dies nicht der beste Weg ist, einen Widgets-Baum in Form von Code zu erstellen.


    Version 5


    Eine weitere Option, die ich versuchen möchte, ist das Erstellen zusätzlicher Typen mit Parametern, die an Konstruktorfunktionen übergeben werden. Zum Beispiel:


    funcBuild()Widget {
        return NewScaffold(ScaffoldParams{
            AppBar: NewAppBar(AppBarParams{
                Title: Text(TextParams{
                    Text: "My Home Page",
                }),
            }),
            Body: NewCenter(CenterParams{
                Child: NewColumn(ColumnParams{
                    MainAxisAlignment: MainAxisAlignment.center,
                    Children: []Widget{
                        Text(TextParams{
                            Text: "You have pushed the button this many times:",
                        }),
                        Text(TextParams{
                            Text:  fmt.Sprintf("%d", m.counter),
                            Style: ctx.textTheme.display1,
                        }),
                    },
                }),
            }),
            FloatingActionButton: NewFloatingActionButton(
                FloatingActionButtonParams{
                    OnPressed: m.incrementCounter,
                    Tooltip:   "Increment",
                    Child: NewIcon(IconParams{
                        Icon: Icons.add,
                    }),
                },
            ),
        })
    }

    Wow! Das ist sehr gut. Hier sind diese Typen ...Paramsetwas auffällig, aber es ist immer noch viel besser als die anderen Optionen. Dieser Ansatz wird übrigens auch häufig in Go-Bibliotheken verwendet und funktioniert besonders gut, wenn Sie nur einige Strukturen haben, die auf diese Weise erstellt werden müssen.


    Tatsächlich gibt es eine Möglichkeit, die Ausführlichkeit zu beseitigen ...Params, dies erfordert jedoch eine Änderung der Sprache. Es gibt sogar einen Vorschlag (Vorschlag) direkt darunter - "untypisierte zusammengesetzte Literale" . Im Wesentlichen bedeutet dies , dass Sie schneiden können FloatingActionButtonParameters{...}auf {...}den Körper der Funktionsparameter. So wird der Code aussehen:


    funcBuild()Widget {
        return NewScaffold({
            AppBar: NewAppBar({
                Title: Text({
                    Text: "My Home Page",
                }),
            }),
            Body: NewCenter({
                Child: NewColumn({
                    MainAxisAlignment: MainAxisAlignment.center,
                    Children: []Widget{
                        Text({
                            Text: "You have pushed the button this many times:",
                        }),
                        Text({
                            Text:  fmt.Sprintf("%d", m.counter),
                            Style: ctx.textTheme.display1,
                        }),
                    },
                }),
            }),
            FloatingActionButton: NewFloatingActionButton({
                    OnPressed: m.incrementCounter,
                    Tooltip:   "Increment",
                    Child: NewIcon({
                        Icon: Icons.add,
                    }),
                },
            ),
        })
    }

    Das passt fast perfekt zu der Version von Dart! Es wird jedoch die Erstellung von Typen für jedes Widget erforderlich.


    Version 6


    Eine weitere Option, die ich testen möchte, ist die Ketteninitialisierung. Ich habe vergessen, wie dieses Muster bezeichnet wird, aber es spielt keine Rolle, da Muster aus Code entstehen müssen und nicht umgekehrt.


    Die Idee ist, dass wir beim Erstellen eines Objekts es zurückgeben, und genau dort können wir die Setter-Methode aufrufen, die ein modifiziertes Objekt zurückgibt - und zwar eins nach dem anderen:


    button := NewButton().
        WithText("Click me").
        WithStyle(MyButtonStyle1)

    oder


    button := NewButton().
        Text("Click me").
        Style(MyButtonStyle1)

    Dann sieht unser Code für das Scaffold Widget so aus:


    // Build renders the MyHomePage widget. Implements Widget interface.func(m *MyHomePage)Build(ctx flutter.BuildContext)flutter.Widget {
        return NewScaffold().
            AppBar(NewAppBar().
                Text("Flutter Go app")).
            Child(NewCenter().
                Child(NewColumn().
                    MainAxisAlignment(MainAxisCenterAlignment).
                    Children([]Widget{
                        Text("You have pushed the button this many times:"),
                        Text(fmt.Sprintf("%d", m.counter)).
                            Style(ctx.Theme.textTheme.display1),
                    }))).
            FloatingActionButton(NewFloatingActionButton().
                Icon(NewIcon(icons.Add)).
                Text("Increment").
                Handler(m.onPressed))
    }

    Dies ist auch kein sehr fremdes Konzept für Go - viele Bibliotheken verwenden es beispielsweise für Konfigurationsoptionen. Es unterscheidet sich syntaktisch etwas von der Dart-Version, weist jedoch alle erforderlichen Eigenschaften auf:


    • explizite Baumkonstruktion
    • benannte "Parameter"
    • Einrückungen, um die Tiefe des Widgets zu verstehen
    • Fähigkeit, Handler und beliebigen Code anzugeben

    In allen Beispielen mag ich die klassische Benennung New...()für Konstruktoren - nur eine Funktion, die ein Objekt erstellt. Es ist viel einfacher, einem Anfänger in der Programmierung zu erklären, als Konstruktoren zu erklären - "dies ist auch eine Funktion, aber ihr Name ist derselbe wie der einer Klasse, aber Sie werden diese Funktion nicht sehen, weil sie etwas Besonderes ist verstehen - dies ist ein Funktions- oder Konstruktorobjekt mit demselben Namen . "


    Von allen Optionen scheinen mir die 5. und 6. die attraktivste zu sein.


    Die endgültige Version des Codes


    Setze alle Teile zusammen und versuche unser "Hallo, Welt" auf ein imaginäres Flutter on Go zu schreiben:


    main.go


    package hello
    import"github.com/flutter/flutter"funcmain() {
        flutter.Run(NewMyApp())
    }

    app.go:


    package hello
    import . "github.com/flutter/flutter"// MyApp is our top application widget.type MyApp struct {
        Core
        homePage *MyHomePage
    }
    // NewMyApp instantiates a new MyApp widgetfuncNewMyApp() *MyApp {
        app := &MyApp{}
        app.homePage = &MyHomePage{}
        return app
    }
    // Build renders the MyApp widget. Implements Widget interface.func(m *MyApp)Build(ctx BuildContext)Widget {
        return m.homePage
    }

    home_page.go:


    package hello
    import (
        "fmt"
        . "github.com/flutter/flutter"
    )
    // MyHomePage is a home page widget.type MyHomePage struct {
        Core
        counter int
    }
    // Build renders the MyHomePage widget. Implements Widget interface.func(m *MyHomePage)Build(ctx BuildContext)Widget {
        return NewScaffold(ScaffoldParams{
            AppBar: NewAppBar(AppBarParams{
                Title: Text(TextParams{
                    Text: "My Home Page",
                }),
            }),
            Body: NewCenter(CenterParams{
                Child: NewColumn(ColumnParams{
                    MainAxisAlignment: MainAxisAlignment.center,
                    Children: []Widget{
                        Text(TextParams{
                            Text: "You have pushed the button this many times:",
                        }),
                        Text(TextParams{
                            Text:  fmt.Sprintf("%d", m.counter),
                            Style: ctx.textTheme.display1,
                        }),
                    },
                }),
            }),
            FloatingActionButton: NewFloatingActionButton(
                FloatingActionButtonParameters{
                    OnPressed: m.incrementCounter,
                    Tooltip:   "Increment",
                    Child: NewIcon(IconParams{
                        Icon: Icons.add,
                    }),
                },
            ),
        })
    }
    // incrementCounter increments app's counter by one.func(m *MyHomePage)incrementCounter() {
        m.counter++
        flutter.Rerender(m)
    }

    Sehr nichts


    Fazit


    Ähnlichkeit mit Vecty


    Ich konnte nicht anders als zu bemerken, wie sehr meine Entscheidung aussieht, wie wir in Vecty Code schreiben . In vielerlei Hinsicht sind sie im Prinzip ähnlich, nur Vecty zeigt das Ergebnis in DOM / CSS / JS an und Flutter verfügt über eine leistungsstarke Render- und Animations-Engine, die von Grund auf neu geschrieben wird, und liefert wunderschöne Grafiken und coole Animationen mit 120 Bildern pro Sekunde. Mir scheint, dass das Design von Vecty sehr erfolgreich ist und meine Lösung für Flutter on Go aus einem bestimmten Grund an Vecty erinnert.


    Besseres Verständnis des Flatterdesigns


    Dieses Gedankenexperiment war an sich schon interessant - nicht jeden Tag müssen Sie Code für ein Framework schreiben, das nicht existiert. Er ließ mich auch tiefer in Flutters Design und technische Dokumentation eintauchen, um besser zu verstehen, was hinter dieser verborgenen Magie steckte.


    Nachteile gehen


    Beantwortung der Frage "Kann Flutter on Go implementiert werden?" Meine Antwort ist definitiv "Ja" , aber ich bin definitiv voreingenommen. Ich weiß nicht, mit wie vielen Einschränkungen und Anforderungen Flutter konfrontiert ist. Im Allgemeinen haben solche Fragen ohnehin keine "richtige" Antwort. Ich war mehr daran interessiert, dass Go in das aktuelle Design passt.


    Das Experiment zeigte, dass das größte Problem bei Go ausschließlich in der Syntax lag . Die Unfähigkeit, eine Funktion aufzurufen und die Namen der Parameter zu übergeben, hat zu erheblichen Schwierigkeiten beim Entwurf eines lesbaren und verständlichen Baums von Widgets geführt. Es gibt Vorschläge für das Hinzufügen benannter Parameter zu zukünftigen Go-Versionen, und es scheint, dass diese Änderungen sogar abwärtskompatibel sind. Aber das Hinzufügen ist etwas anderes zu lernen, eine andere Wahl vor jeder Funktion usw. Die kumulativen Vorteile sind also nicht so offensichtlich.


    Ich habe keine Probleme mit dem Fehlen von Generika oder Ausnahmen in Go. Wenn Sie den besten Weg kennen, um die beschriebene Aufgabe im Experiment mit Hilfe von Generika zu lösen - schreiben Sie bitte die Kommentare mit Codebeispielen, ich bin aufrichtig interessiert, sie zu hören.


    Gedanken zur Zukunft des Flatterns


    Mein letzter Gedanke wird sein, dass Flutter ungewöhnlich gut ist, trotz all des Grollens, das ich mir in diesem Artikel erlaubte. Das "Coolness / Bad" -Verhältnis ist überraschend groß und Dart ist ziemlich leicht zu verstehen (zumindest für Leute, die mit anderen Programmiersprachen vertraut sind). In Anbetracht des Dart-Browser-Stammbaums träume ich davon, dass eines Tages alle Browser-Engines (obwohl noch viele vorhanden sind) mit DartVM statt mit V8 kompatibel sind und Flutter nativ integriert wird - und alle Flutter-Anwendungen werden automatisch auch Webanwendungen sein.


    Die Arbeit an dem Rahmen ist nur astronomisch. Dies ist ein Projekt von höchster Qualität und einer ausgezeichneten und wachsenden Gemeinschaft. Zumindest die Anzahl der unnötig hochwertigen Materialien und Tutorials ist im Hinblick auf das Framework, dessen Version 1.0 vor weniger als einem Monat veröffentlicht wurde, einfach überwältigend. Ich hoffe, dass ich irgendwann auch zu dem Projekt beitragen kann.


    Für mich ist dies ein Game-Changer, und ich hoffe, Flutter so gut wie möglich zu beherrschen und mobile Anwendungen für mich selbst und zum Vergnügen zu schreiben, denn dies wird nicht mehr vielen Unternehmen mit mobilen Entwickler-Mitarbeitern vorbehalten sein.


    Auch wenn Sie sich selbst noch nie als Entwickler einer mobilen Benutzeroberfläche gesehen haben, versuchen Sie Flutter, einen Hauch frischer Luft.


    Links



    Jetzt auch beliebt: