Главная » Хабрахабр » Мысленный эксперимент: Flutter на Go

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget
} 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().

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

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

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

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

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

type MyApp struct { // ...
}

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

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

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

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

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

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

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

Состояние

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

Но дальше – больше, вы не можете просто так унаследовать другой класс (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() }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Как логическое продолжение темы, давайте взглянём, как "виджет с состоянием" используется в другом виджете в 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, чтобы понять как эта магия устроена, и вряд ли будет понимать, как и что взаимосвязано. Я считаю это сущим злом в программировании — прятать и скрывать от программиста как можно больше, оправдывая это эргономикой.

Подход Flutter на Dart, наверняка, тоже можно реализовать, но я люблю Go за минимизацию магии, и эту же философию хотел бы видеть в фреймворках. Для 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, который добавляет ещё больше магии и позволяет делать виджеты со стейтом на лету.

Как мы будем представляеть дерево виджетов на 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.

Версия 1

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

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

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

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

Версия 2

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

import . "github.com/flutter/flutter"

Text мы можем написать просто Text. Теперь вместо flutter. Из моей практики, это именно тот случай, для которого подобный импорт является допустимым – например, как при использовании замечательного фреймворка для тестирования 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

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

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

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

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

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

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

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

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

Это сильно проще объяснять новичку в программировании, чем объяснять конструкторы — "это тоже функция, но у неё имя такое же, как у класса, но ты не увидишь эту функцию, потому что она специальная, и просто глядя на функцию, ты не можешь легко понять – это функция или конструктор объекта с таким именем". Также во всех примерах мне нравится использование классического именования 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 выводит результат в DOM/CSS/JS, а Flutter под собой несёт мощный и написанный с нуля движок рендеринга и анимаций, дающий красивейшую графику и крутую анимацию на 120 кадрах в секунду. Я не мог не обратить внимание на то, как сильно моё решение напоминает то, как мы пишем код на Vecty. Но мне кажется, что дизайн Vecty очень удачен, и моё решение для Flutter на Go напоминает Vecty неспроста.

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

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

Недостатки Go

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

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

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

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

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

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

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

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Профессиональная IoT-конференция InoThings++ — что было и что будет

Привет, Хабр! Практически ровно год назад — в конце января 2018-го — мы попробовали провести первую профессиональную конференцию для разработчиков устройств, систем и проектов «Интернета вещей» InoThings++ 2018. Помимо того, что она была первой для нас — если не считать ...

[Перевод] Изучаем Python: модуль argparse

Если вы занимаетесь обработкой и анализом данных с использованием Python, то вам, рано или поздно, придётся выйти за пределы Jupyter Notebook, преобразовав свой код в скрипты, которые можно запускать средствами командной строки. Здесь вам и пригодится модуль argparse. Для новичков, ...