Pull to refresh

Мысленный эксперимент: Flutter на Go

Reading time 21 min
Views 21K

Совсем недавно я открыл для себя Flutter – новый фреймворк от Google для разработки кроссплатформенных мобильных приложений – и даже имел возможность показать основы Flutter человеку, который никогда не программировал до этого. Сам Flutter написан на Dart – языке, родившимся в браузере Chrome и сбежавшим в мир консоли –  и это навело меня на мысль "хм, а ведь Flutter мог вполне бы быть написан на Go!".


Ведь почему нет? И Go и Dart созданы Google, оба типизированные компилируемые языки – повернись некоторые события чуть иначе, Go был бы отличным кандидатом для реализации такого масштабного проекта, как Flutter. Кто-то скажет – в Go нет классов, дженериков и исключений, поэтому он не подходит.


Так давайте представим, что Flutter уже написан на Go. Как будет выглядеть код и вообще, получится ли это?



Что не так с Dart?


Я следил за этим языком с самого его зарождения в качестве альтернативы JavaScript в браузерах. Dart какое-то время был встроен в браузер Chrome и надежда была на то, что он вытеснит JS. Безумно грустно было читать в марте 2015 года, что поддержка Dart была убрана из Chrome.


Сам Dart великолепен! Ну, в принципе, после JavaScript любой язык великолепен, но после, скажем, Go, Dart не настолько прекрасен. но вполне ок. В нём есть все мыслимые и немыслимые фичи – классы, дженерики, исключения, futures, async-await, event loop, JIT/AOT, сборщик мусора, перегрузка функций – назовите любую известную фичу из теории языков программирования и в Dart она будет с высокой долей вероятности. У Dart есть специальный синтаксис для почти любой фишки – специальный синтаксис для геттеров/сеттеров, специальный синтаксис для сокращённых конструкторов, специальный синтаксис для специального синтаксиса и много чего другого.


Это делает Dart прямо с первого взгляда знакомым для людей, которые уже программировали на любом языке программирования до этого, и это отлично. Но пытаясь объяснить всё это обилие специальных фич в простеньком "Hello, world" примере, я обнаружил, что это, наоборот, затрудняет освоение.


  • все "специальные" фичи языка запутывали – "специальный метод под названием конструктор", "специальный синтаксис для автоматической инициализации", "специальный синтаксис для именованных параметров" и т.д.
  • все "скрытое" запутывало – "из какого импорта эта функция? это скрыто, глядя на код узнать это нельзя", "почему в этом классе есть конструктор, а в этом нет? он там есть, но он скрыт" и так далее
  • всё "неоднозначное" запутывало – "так тут создавать параметры функции с именами или без?", "тут должно быть const или final?", "тут использовать нормальный синтаксис функции или ''сокращённый со стрелочкой''" и т.д.

В принципе эта троица – "специальный", "скрытый" и "неоднозначный" – неплохо улавливает суть того, что люди называют "магией" в языках программирования. Это фичи, созданные для упрощения написания кода, но по факту усложняющие его чтения и понимание.


И это именно та область, где Go занимает принципиально отличную позицию от других языков, и яростно держит оборону. Go это это язык практически без магии – количество "скрытого", "специального" и "двусмысленного" в нём сведено до минимума. Но у Go есть свои недостатки.


Что не так с Go?


Поскольку мы говорим о Flutter, а это UI фреймворк, давайте рассмотрим Go как инструмент для описания и работы с UI. Вообще, UI фреймворки это колоссальнейшей сложности задача и почти всегда требует специализированных решений. Один из самых частых подходов в UI это создание DSL – предметно-ориентированных языков – реализованных в виде библиотек или фреймоврков, заточенных конкретно под нужды UI. И чаще всего можно услышать мнение, что Go объективно плохой язык для DSL.


По сути, DSL означает создание нового языка – терминов и глаголов – которыми сможет оперировать разработчик. Код на нём должен внятно описывать главные черты графического интерфейса и его компонентов, быть достаточно гибким, чтобы давать волю фантазии дизайнера, и при этом быть достаточно жёстким, чтобы ограничивать её же в соответствии с некими правилами. К примеру, вы должны иметь возможность разместить кнопки на некотором контейнере, а в эту кнопки поместить иконку в нужном месте, но при этом компилятор должен вернуть ошибку, если вы попытаетесь вставить кнопку в, скажем, текст.


Плюс, языки для описания UI часто декларативные – давая возможность описать интерфейс в виде "что хотелось бы видеть", и позволить фреймворку самому из этого понять, какой код и как запускать.


Некоторые языки изначально разрабатывались с такими задачами на прицеле, но не Go. Похоже, что написать Flutter на Go будет та ещё задача!


Ода Flutter


Если вы ещё не знакомы с Flutter, то я настойчиво рекомендую потратить следующие выходные за просмотром обучающих видео или чтении туториалов, коих множество. Потому как Flutter, безо всякого сомнения, переворачивает правила игры в разработке мобильных приложений. И, вполне вероятно, не только мобильных – уже есть рендереры (в терминах Flutter, embedders) для того, чтобы запускать Flutter приложения как нативные dekstop-приложения, и как веб-приложения.


Он легко учится, он логичен, идёт с огромнейшей библиотекой красивейших виджетов на Material Design (и не только), у него великолепное и большое коммьюнити и отличный тулинг (если вам нравится легкость работы с go build/run/test в Go, то в Flutter вы получите похожий опыт).


Ещё год назад мне нужно было написать небольшое мобильное приложение (под iOS и Android, разумеется), и я понимал, что сложность разработки качественного приложения под обе платформы слишком велика (приложение было не основной задачей) – пришлось аутсорсить и платить за него деньги. По факту, написать несложное, но качественное и работающее на всех устройствах приложение было неподъемной задачей даже для человека с почти 20 летним опытом программирования. И это всегда был нонсенс для меня.


С Flutter я переписал это приложения за 3 вечера, при этом изучая сам фреймворк с нуля. Если бы мне кто-то рассказал что так может быть чуть ранее, я бы не поверил.


Последний раз, когда я видел подобный буст продуктивности с открытием новой технологии – это 5 лет назад, когда я открыл для себя Go. Тот момент изменил мою жизнь.


Так что рекомендую начать изучение Flutter и вот этот туториал очень хорош.


"Hello, World" на Flutter


Когда вы создаёте новое приложение через flutter create, вы получите вот такую программу с заголовком, текстом, счётчиком и кнопкой, увеличивающей счётчик.



Мне кажется это отличный пример. чтобы написать его на нашем воображаемом Flutter на Go. В нём есть почти все основные концепты фреймворка, на которых можно проверить идею. Давайте посмотрим на код (это один файл):


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),
      ),
    );
  }
}

Давайте разберём код по частям, проанализируем что и как ложится на Go, и взглянем на различные варианты, которые у нас есть.


Переводим код на Go


Начало будет простым и незамысловатым – импорт зависимости и запуск функции main(). Ничего сложного или интересного тут, изменение практически синтаксическое:


package hello

import "github.com/flutter/flutter"

func main() {
    app := NewApp()
    flutter.Run(app)
}

Единственное отличие лишь в том, что вместо запуска MyApp() — функции, которая является конструктором, которая есть специальной функцией, которая спрятана внутри класса с именем MyApp – мы просто вызываем обычную явную и не спрятанную функцию NewApp(). Она делает тоже самое, но гораздо понятней объяснять и понимать, что это такое, как запускается и как работает.


Классы виджетов


В Flutter всё состоит из виджетов. В Dart-версии Flutter каждый виджет реализован в виде класса, который наследует специальные классы для виджетов из Flutter.


В Go нет классов, и, соответственно, иерархии классов, потому что мир не объектно-ориентирован, и уж тем более не иерархичен. Для программистов, знакомым только с класс-ориентированной моделью ООП это может быть откровением, но это действительно не так. Мир – это гигантский переплетённый граф концепций, процессов и взаимодействий. Он не идеально структурирован, но и не хаотичен, и попытка втиснуть его в иерархии классов – это самый надёжный способ сделать кодовую базу нечитабельной и неповоротливой – именно то, что представляют из себя большинство кодовых баз на данное время.



Я очень ценю Go за то, что его создатели потрудились переосмыслить этот вездесущий концепт классов и реализовали в Go гораздо более простой и мощный концепт ООП, который, не случайно, оказался ближе к тому, что создатель ООП, Алан Кей, имел ввиду.


В Go мы представляем любую абстракцию в виде конкретного типа – структуры:


type MyApp struct {
    // ...
}

В Dart-версии Flutter, MyApp должен унаследовать StatelessWidget и переопределить метод build. Это нужно для решения двух задач:


  1. дать нашему виджету (MyApp) некие специальные свойства/методы
  2. дать возможность Flutter вызывать наш код в процессе построения/рендеринга

Я не знаю внутренностей Flutter, поэтому допустим, что пункт номер 1 не под вопросом, и мы просто должны это сделать. В Go для такой задачи есть единственное и очевидное решение – встраивание (embedding) типов:


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

Этот код добавит все свойства и методы flutter.Core к нашему типу MyApp. Я назвал его Core вместо Widget, потому что, во-первых, встраивание типа ещё не делает наш MyApp виджетом, а, во-вторых, это название очень удачно используется в GopherJS фреймворке Vecty (что-то вроде React, только для Go). Я коснусь темы похожести Vecty и Flutter чуть позднее.


Второй момент – реализация метода build(), который сможет использовать движок Flutter – также в Go решается просто и однозначно. Мы лишь должны добавить метод с определённой сигнатурой, удовлетворяющей некоему интерфейсу, определённому где-нибудь в библиотеке нашего вымышленного Flutter на Go:


flutter.go:


type Widget interface {
    Build(ctx BuildContext) Widget
}

И теперь наш 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()
}

Мы можем заметить несколько отличий тут:


  • код несколько более многословен – BuildContext, Widget и MaterialApp указывают на импорт flutter перед ними.
  • код несколько менее голословен – нет слов вроде extends Widget или @override
  • метод Build() начинается с заглавной буквы, потому что это означает "публичность" метода в Go. В Dart публичность определяется тем, начинается имя со знака подчёркивания (_) или нет.

Итак, чтобы сделать виджет в нашем Flutter на Go, нам необходимо встроить тип flutter.Core и реализовать интерфейс flutter.Widget. С этим разобрались, копаем дальше.


Состояние


Вот это была одна из вещей, которая меня сильно смутила во Flutter. Есть два разных класса – StatelessWidget и StatefulWidget. Как по мне, "виджет без состояния" это такой же виджет, просто без, хм, данных, состояния – зачем тут придумывать новый класс? Но окей, я могу с этим жить.


Но дальше – больше, вы не можете просто так унаследовать другой класс (StatefulWidget), а должны написать вот такую магию (IDE сделает это за вас, но не суть):


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()
  }
}

Мда уж, давайте разберемся, что тут происходит.


Фундаментально задача стоит так: добавить к виджету состояние (state) –  счётчик, в нашем случае – и дать механизм движку Flutter узнавать, когда мы изменили состояние, чтобы перерисовать виджет. Это реальная сложность задачи (essential complexity в терминах Брукса).


Всё остальное – это добавочная сложность (accidental complexity). Flutter на Dart придумывает новый класс State, который использует дженерики и принимает виджет в качестве параметра типа. Далее, создаётся класс _MyHomePageState, который наследует State виджета MyApp… окей, это ещё можно как-то переварить. Но почему метод build() определяется у класса State, а не у класса который виджет? Бррр....


Ответ на этот вопрос есть в Flutter FAQ и достаточно подробно рассмотрен тут и краткий ответ – чтобы избежать определённого класса багов при наследовании StatefulWidget. Другими словами, это обходной путь для решения проблемы класс-ориентированного ООП дизайна. Шик.


Как бы мы сделали это в Go?


Во-первых, я бы лично всеми силами предпочёл не создавать отдельную сущность для "состояния" – State. Ведь мы уже и так имеем состояние в каждом конкретном типе – это просто поля структуры. Язык уже нам дал эту сущность, так сказать. Создавать ещё одну аналогичную сущность будет лишь запутывать программиста.


Задача, конечно же, состоит в том, чтобы дать Flutter возможность реагировать на изменение состояния (это суть реактивного программирования, как-никак). И если мы можем "попросить" разработчика использовать специальную функцию (setState()), то аналогично можем взамен попросить использовать специальную функцию, чтобы говорить движку, когда надо перерисовывать, а когда нет. В конце-концов, не все изменения состояния требуют перерисовки, и тут у нас будет даже больший контроль:


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()
}

Можно поиграться с различными вариантами именования – мне нравится NeedsUpdate() за прямоту и тем, что это свойство виджета (полученное от flutter.Core), но глобальный метод flutter.Rerender() тоже выглядит неплохо. Правда он даёт ложное чувство того, что виджет вот прям немедленно перерисуется, но это не так – он перерисуется на следующем обновлении кадра, а частота вызова метода может быть сильно выше частоты отрисовки – но с этим уже должен разбираться наш движок Flutter.


Но идея в том, что мы только что решили необходимую задачу без добавления:


  • нового типа
  • дженериков
  • специальных правил для чтения/записи состояния
  • специальных новых переопределённых методов

Плюс, API намного яснее и понятнее – просто увеличиваем счётчик (как это делали ли бы в любой другой программе) и просим Flutter перерисовать виджет. Это как раз то, что не сильно очевидно, если бы мы просто вызывали setState – которая не просто специальная функция для установки состояния, это функция, которая возвращает функцию (wtf?), в которой мы уже что-то делаем с состоянием. Опять же, скрытая магия в языках и фреймворках сильно затрудняет понимание и читабельность кода.


В нашем же случае, мы решили ту же задачу, код проще и короче в два раза.


Виджеты с состоянием в других виджетах


Как логическое продолжение темы, давайте взглянём, как "виджет с состоянием" используется в другом виджете в Flutter:


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

MyHomePage тут это "виджет с состоянием" (у него есть счётчик), и мы создаём его вызывая конструктор MyHomePage() во время билда… Постойте, что-что?


build() вызывается для перерисовки виджета, вполне возможно много раз в секунду. Почему мы должны создавать виджет, тем более с состоянием, каждый раз во время отрисовки? Это не имеет смысла.


Оказывается, Flutter использует это разделение между Widget и State для того, чтобы спрятать вот эту инициализацию/менеджмент состояния от программиста (больше спрятанных вещей, больше!). Он создаёт новый виджет каждый раз, но состояние, если уже было создано, находит автоматически и прикрепляет к виджету. Эта магия происходит невидимо и я без понятия, как именно это работает – надо читать код.


Я считаю это сущим злом в программировании — прятать и скрывать от программиста как можно больше, оправдывая это эргономикой. Уверен, среднестатический программист не будет читать код Flutter, чтобы понять как эта магия устроена, и вряд ли будет понимать, как и что взаимосвязано.


Для Go версии я бы однозначно не хотел такого вот скрытого колдовства, и предпочёл бы явную и видимую инициализацию, даже если это означает чуть более голословный код. Подход Flutter на Dart, наверняка, тоже можно реализовать, но я люблю Go за минимизацию магии, и эту же философию хотел бы видеть в фреймворках. Поэтому, мой код для виджетов с состоянием в дереве виджетов я бы писал так:


// MyApp is our top application widget.
type MyApp struct {
    flutter.Core
    homePage *MyHomePage
}

// NewMyApp instantiates a new MyApp widget
func NewMyApp() *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)
}

Этот код проигрывает версии на Dart в том, что если я захочу убрать homePage из дерева виджетов и заменить на что-то другое, то мне придётся убирать его в трёх местах, вместо одного. Но взамен мы получаем полную картинку того, что, где и как происходит, где выделяется память, кто кого вызывает и так далее – код на ладони, понятен и легкочитаем.


Кстати, у Flutter есть ещё такая вещь как StatefulBuilder, который добавляет ещё больше магии и позволяет делать виджеты со стейтом на лету.


DSL


Теперь возьмемся за самую веселую часть. Как мы будем представляеть дерево виджетов на Go? Мы хотим, чтобы оно выглядело кратко, чисто, было легким в рефакторинге и изменениях, описывало пространственные взаимосвязи между виджетами (виджеты, которые визуально рядом, должны быть рядом и в описании), и, при этом, достаточно гибким, чтобы описывать в нём произвольный код вроде обработчиков событий.


Мне кажется вариант на Dart достаточно красив и красноречив:


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),
      ),
);

У каждого виджета есть конструктор, который принимает опциональные параметры, и что делает тут запись действительно симпатичной это именованные параметры функций.


Именованные параметры


На случай, если вы не знакомы с этим термином, то во многих языках параметры функции называются "позиционными", так как для функции имеет значение их позиция:


Foo(arg1, arg2, arg3)

, а в случае с именованными параметрами, всё решает их имя в вызове:


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

Это добавляет текста, но сохраняет клики и перемещения по коду, в попытках понять, что параметры означают.


В случае с деревом виджетов, они играют ключевую роль в читабельности. Сравните тот же код, что и выше, но без именованных параметров:


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),
      ),
    );

Не то. правда? Его не только сложнее понимать (нужно держать в памяти, что означает каждый параметр и каков его тип, и это существенная когнитивная нагрузка), но и также не даёт нам свободы в выборе какие параметры мы хотим передать. Например, вы можете не хотеть для вашего Material приложения FloatingActionButton, поэтому вы просто его не указываете в параметрах. Без именованных параметров, нам придётся либо принуждать указывать все возможные виджеты, либо прибегать к магии с reflection, чтобы узнать, какие именно виджеты были переданы.


И так как в Go нет перегрузки функций и именованных параметров, то это будет непростая задача для Go.


Дерево виджетов в Go


Версия 1


Давайте ближе взглянем на объект Scaffold, который представляет из себя удобную обёртку для мобильного приложения. У него есть несколько свойств – appBar, drawe, home, bottomNavigationBar, floatingActionBar – и это всё виджеты. Создавая дерево виджетов, мы фактически должны как-то инициализировать этот объект, передав ему вышеупомянутые свойства-виджеты. Ну, это не слишком отличается от обычного создания и инициализации объектов.


Давайте попробуем подход "в лоб":


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,
    ),
)

Не самый красивый UI код, однозначно. Слово flutter повсюду и так и просится. чтобы его спрятать (вообще-то, я должен был назвать пакет material, а не flutter, но не суть), безымянные параметры совершенно неочевидны, а эти nils повсюду откровенно сбивают с толку.


Версия 2


Поскольку всё равно большая часть кода будет использовать тот или иной тип/функцию из пакета flutter, мы можем использовать "точечный импорт" (dot import) формат, чтобы импортировать пакет в наше пространство имён и, тем самым, "спрятать" имя пакета:


import . "github.com/flutter/flutter"

Теперь вместо flutter.Text мы можем написать просто Text. Это обычно плохая практика, но мы же работает с фреймворком, и этот импорт будет буквально в каждой строчке. Из моей практики, это именно тот случай, для которого подобный импорт является допустимым – например, как при использовании замечательного фреймворка для тестирования GoConvey.


Давайте посмотрим, как будет выглядеть код:


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,
    ),
)

Уже лучше, но эти nil-ы и неименованные параметры....


Версия 3


Давайте посмотрим, как будет выглядеть код, если мы используем reflection (возможность инспекции кода во время работы программы) для анализа переданных параметров. Такой подход используется в нескольких ранних HTTP-фреймворках на Go (martini, например), и считается очень плохой практикой – он небезопасен, теряет удобство системы типов, относительно медленный и добавляет магию в код – но ради эксперимента можно попробовать:


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,
    ),
)

Неплохо, и похоже на оригинальную версию из Dart, но нехватка именованных параметров всё равно сильно режет глаз.


Версия 4


Давайте немного отступим назад и зададимся вопросом – что собственно мы пытаемся сделать. Нам необязательно слепо копировать подход Dart (хотя это будет приятный бонус – меньше нового учить людям, уже знакомым с Flutter на Dart). По сути, мы просто создаём новые объекты и присваиваем им свойства.


Может попробовать вот таким способом?


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

Такой подход будет работать, и хоть он и решает нашу проблему с "именованными параметрами", он таки сильно запутывает картинку дерева виджетов в целом. Во-первых, порядок определения виджетов меняется на обратный – чем глубже виджет, тем раньше он должен быть создан. Во-вторых, мы потеряли удобную визуальную структуру кода, которая даже с помощью отступов быстро давала понять глубину виджета в дереве.


Кстати, этот подход использовался очень давно в UI фреймворках вроде GTK или Qt. Посмотрите, например, на код из документации последнего Qt 5:


 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);

Поэтому вполне допускаю, что для кого-то такой формат будет привычней и роднее. Но, всё же, тяжело спорить с тем, что это не наилучший способ построения дерева виджетов в виде кода.


Версия 5


Ещё один вариант, который я хочу попробовать – это создание дополнительных типов с параметрами для передачи в функции-конструкторы. Например:


func Build() 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,
                }),
            },
        ),
    })
}

Ухты! Это, очень даже неплохо. Вот эти типы ...Params немного бросаются в глаза, но всё равно это сильно лучше остальных вариантов пока что. Такой подход, кстати, довольно часто используется и библиотеках на Go и особенно хорошо работает, когда у вас есть лишь пару структур, которые нужно создавать таким образом.


Вообще-то, есть способ убрать многословность ...Params, но для этого потребуется изменение в языке. Есть даже предложение (proposal) как раз под это — "нетипизированные составные литералы". По сути, это означает возможность сократить FloatingActionButtonParameters{...} до {...} в теле параметров функции. Вот как будет выглядеть код:


func Build() 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,
                }),
            },
        ),
    })
}

Это почти идеальное совпадение с версией на Dart! Хотя она и потребует создание типов для каждого виджета.


Версия 6


Ещё один вариант, который я хотел бы протестировать это инициализация параметров цепочкой. Я забыл, как этот паттерн называется, но и не важно, потому что паттерны должны рождаться из кода, а не наоборот.


Идея в том, что при создании объекта, мы его возвращаем, и тут же можем вызывать метод-сеттер, который возвращает изменённый объект – и так один за другим:


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

или


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

Тогда наш код для Scaffold-виджета будет выглядеть вот так:


// 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))
}

Это тоже не сильно чужеродный концепт для Go – многие библиотеки его используют для опций конфигурации, например. Он синтаксически несколько отличается от Dart-версии, но всё таки обладает всеми необходимыми свойствами:


  • явное построение дерева
  • именованные "параметры"
  • отступы помогающие понять глубину виджета
  • возможность указывать обработчики и произвольный код

Также во всех примерах мне нравится использование классического именования New...() для конструкторов – просто функция, которая создаёт объект. Это сильно проще объяснять новичку в программировании, чем объяснять конструкторы — "это тоже функция, но у неё имя такое же, как у класса, но ты не увидишь эту функцию, потому что она специальная, и просто глядя на функцию, ты не можешь легко понять – это функция или конструктор объекта с таким именем".


Так или иначе, из всех вариантов, 5-й и 6-й мне кажутся наиболее привлекательными.


Финальная версия кода


Соберём все части вместе и попробуем записать наш "hello, world" на воображаемом Flutter на Go:


main.go


package hello

import "github.com/flutter/flutter"

func main() {
    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 widget
func NewMyApp() *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)
}

Очень даже ничего!


Заключение


Похожесть с Vecty


Я не мог не обратить внимание на то, как сильно моё решение напоминает то, как мы пишем код на Vecty. Во многом, они, в принципе, похожи, только Vecty выводит результат в DOM/CSS/JS, а Flutter под собой несёт мощный и написанный с нуля движок рендеринга и анимаций, дающий красивейшую графику и крутую анимацию на 120 кадрах в секунду. Но мне кажется, что дизайн Vecty очень удачен, и моё решение для Flutter на Go напоминает Vecty неспроста.


Лучшее понимание дизайна Flutter


Этот мысленный эксперимент был интересен сам по себе – не каждый день приходится писать код на фреймворке, которого не существует. Но он также заставил меня глубже копнуть дизайн и техническую документацию Flutter, чтобы лучше понять что стоит за той скрытой магией.


Недостатки Go


Отвечая на вопрос "Может ли Flutter быть реализован на Go?" мой ответ однозначное "да", но я, безусловно, предубеждён, наверняка ещё не знаю массу ограничений и требований, стоящих перед Flutter, и, вообще, такие вопросы не имеют "правильного" ответа всё равно. Я больше был заинтересован в том, что именно в Go хорошо или плохо ложится на нынешний дизайн.


Эксперимент продемонстрировал, что наибольшая проблема с Go была исключительно в синтаксисе. Невозможность вызывать функцию и передать имена параметров создала существенные затруднения в дизайне читабельного и понятного дерева виджетов. Есть предложения по добавлению именованных параметров в будущие версии Go, и, похоже, эти изменения даже обратно-совместимые. Но их добавление – это ещё одна вещь для изучения, ещё один выбор перед каждой функцией и так далее, так что кумулятивная польза не так уж очевидна.


Я не встретил проблем с отсутствием дженериков или исключений в Go. Если вам известен лучший способ достичь описанной задачи в эксперименте с помощью дженериков – напишите пожалуйста в комментариях с примерами кода, я буду искренне заинтересован их услышать.


Мысли о будущем Flutter


Мои заключительные мысли будут о том, что Flutter необыкновенно хорош, несмотря на всё то бурчание, которое я себе позволил в этой статье. Соотношение "крутота/так себе" на удивление велико, и Dart достаточно легко схватывается (как минимум, людям, знакомым с другими языками программирования). Учитывая браузерную родословную Dart, я мечтаю, что однажды все браузерные движки (хотя, сколько их там осталось) будут идти с DartVM вместо V8, и Flutter будет интегрирован нативно – и все Flutter приложения автоматически будут также и веб-приложениями.


Работа, проделанная над фреймворком просто астрономическая. Это проект высочайшего качества и с отличным и растущим комьюнити. Как минимум, количество неоправданно качественных материалов и туториалов просто ошеломительное как для фреймворка, версия 1.0 которого вышла меньше месяца назад. Надеюсь, когда-нибудь также внести свою лепту в проект.


Для меня это game changer, и я надеюсь освоить Flutter настолько, насколько возможно и писать мобильные приложения для себя и ради удовольствия, ибо это больше не будет удел компаний со штатом мобильных разработчиков.


Даже если вы никогда не видели себя в качестве разработчиках мобильных UI – попробуйте Flutter, это глоток свежего воздуха.


Ссылки


Tags:
Hubs:
+20
Comments 73
Comments Comments 73

Articles