SimbirSoft corporate blog
Development of mobile applications
Dart
Flutter
27 February

Flutter. Плюсы и минусы



В первую очередь расскажем немного о том, что же такое Flutter. Это фреймворк для создания мобильных приложений от компании Google. Он является кроссплатформенным и позволяет компилировать созданный проект под 3 операционные системы:
  • Android
  • iOS
  • Fuchsia

Причем для последней ОС – Fuchsia – это пока единственный способ создать приложение.
Flutter на протяжении долгого времени, с 2015 года, был представлен только в альфа и бета версиях. Релиз первой стабильной версии состоялся 4 декабря 2018 года.


Flutter активно продвигается Google, постепенно набирает популярность и, скорее всего, в дальнейшем будет теснить другие, используемые сейчас средства кроссплатформенной разработки (React Native, Xamarin), особенно при условии широкого распространения Fuchsia. С учетом того, что Google позиционирует данную операционную систему как замену Android, рано или поздно Flutter вытеснит нативную разработку под Android. Поэтому перспективность и активное развитие – основные плюсы Flutter.


+ Перспективность и активное развитие


Давайте разберемся, как это работает.


На языке программирования Dart создается мобильное приложение с описанием графического интерфейса и всей логики работы. Результат работы добавляется в нативное приложение, как и картинки, шрифты и тому подобное (разумеется, этот процесс автоматизирован).


Одновременно в нативной части приложения создается один-единственный экран, где подгружается виртуальная машина Dart, которая и выполняет Flutter.


Заметим, что отсюда следует один из минусов Flutter:


— Конечный установочный пакет больше, так как в него добавляется виртуальная машина Dart.


Таким образом, есть файлы Flutter и есть виртуальные машины, которые добавляются в зависимости от того, что компилируется – iOS или Android.


В составе виртуальной машины есть собственный графический движок, он рисует интерфейс приложения со всеми переходами между экранами, диалогами, фрагментами и т.д. В этом разработка под Flutter значительно отличается от разработки с Xamarin и React Native, которые используют реальные Android и iOS компоненты. В случае с ними невозможно использовать специфические для платформы компоненты (если такая необходимость есть, приходится создавать два варианта UI). С Flutter при выборе дизайна достаточно ориентироваться на одну платформу (например, Android). При сборке проекта под iOS вы увидите стандартный Android интерфейс. Это будет выглядеть немного странно и неожиданно, но вполне работоспособно (впоследствии интерфейс можно доработать).


+ Собственный графический движок (нет необходимости делать интерфейс отдельно для Android и iOS)


Теперь о впечатлениях.


В ходе портирования нескольких приложений с Android на Flutter мы отметили некоторые различия, которые можно считать как плюсом, так и минусом.


Первое, что бросается в глаза – это способ создания экранов, который значительно отличается от используемых на Android и iOS. В Android разделена логика и интерфейс: логика задается кодом, а интерфейс – версткой в xml. На Flutter все это задается с помощью кода. Хотя здесь для интерфейса используется особый стиль – элементы интерфейса создаются вложенными друг в друга. Это немного похоже на верстку, очень похожий способ действует в React Native. При этом отсутствует возможность прямого доступа к элементам. Чтобы что-то изменить на экране, нужно либо обновить весь экран, либо воспользоваться специальными контроллерами, заблаговременно добавленными к виджету во время его создания.


— Интерфейс создается с помощью кода, из-за чего грань между логикой и дизайном гораздо тоньше.


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


+ Интерфейс легко разбивается на отдельные модули


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


Сделаем три вкладки:


1) Первая – с текстом и ползунками для настройки размера и цвета текста
2) На вторую добавим загружаемую картинку (с индикатором прогресса)
3) На третьей поместим пример списка




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

Теперь рассмотрим исходный код, который реализует такую верстку:


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
 // This widget is the root of your application.
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Flutter Demo',
     theme: ThemeData(
       primarySwatch: Colors.blue,
     ),
     home: MyHomePage(title: 'Home Page'),
   );
 }
}

class MyHomePage extends StatefulWidget {
 MyHomePage({Key key, this.title}) : super(key: key);

 final String title;

 @override
 _MyHomePageState createState() => _MyHomePageState();
}

Данный фрагмент кода является стандартным для практически любого Flutter приложения (он создается вместе с проектом).


MyApp – это класс самого приложения, в котором при создании MaterialApp описываются общие параметры: название приложения, шрифты, цвета и стили. Также здесь указывается основной экран приложения (для нас это MyHomePage).


Сделаем важное замечание: во Flutter виджеты разделяются на два типа:


1) StatefulWidget
2) StatelessWidget


Для описания StatefulWidget требуется два класса: класс самого виджета и класс его состояния (в котором и будет происходить основная работа).


StatelessWidget описывается одним классом с фиксированным состоянием, и его можно изменить только путем пересоздания из основного виджета. Поэтому для наших целей требуется именно StatefulWidget.


Теперь рассмотрим _MyHomePageState:


class _MyHomePageState extends State<MyHomePage> {
 int _currentIndex = 0;
 double _size = 14;
 double _r = 0;
 double _g = 0;
 double _b = 0;

 @override
 Widget build(BuildContext context) {
   return Scaffold(
       appBar: AppBar(
         title: Text(widget.title),
       ),
       body: <Widget>[

Для простоты восприятия красным цветом помечена вкладка с текстом, зеленым – вкладка с картинкой, синим – вкладка со списком, а желтым – навигационное меню. Как можно заметить, интерфейс описывается как множество вложенных друг в друга виджетов (и их массивов):










Используемые функции:
 void _onTapped(int index) {
   setState(() {
     _currentIndex = index;
   });
 }

 void _setTextStyle(
     {double size = -1, double r = -1, double g = -1, double b = -1}) {
   setState(() {
     if (size > 0) {
       _size = size;
     }
     if (r > 0) {
       _r = r;
     }
     if (g > 0) {
       _g = g;
     }
     if (b > 0) {
       _b = b;
     }
   });
 }
}

Рассмотрим их немного поподробнее:


onTapped – функция, вызываемая при переключении вкладки в нижнем меню. В ней вызывается специальная функция setState, которая позволяет обновить текущий виджет с новыми данными (а мы обновили переменную _currentIndex).


Посмотрим, где она применяется:


body: <Widget>[
        текст
        картинка
        список
        ][_currentIndex]

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


Далее идет функция _setTextStyle. Она имеет весьма необычное объявление для C-подобных языков.


void _setTextStyle({double size = -1, double r = -1,
                       double g = -1,double b = -1})

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


Так как каждый аргумент именован, то мы можем брать их в произвольном порядке. Например:


_setTextStyle(size: 24, b: 255)

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


Первая вкладка:


class TextWidget extends StatefulWidget {
 @override
 _TextWidgetState createState() => _TextWidgetState();
}

class _TextWidgetState extends State<TextWidget> {
 double _size = 14;
 double _r = 0;
 double _g = 0;
 double _b = 0;

 @override
 Widget build(BuildContext context) {
   return Column(
     children: <Widget>[
       Text("Example String",
           style: TextStyle(
               fontSize: _size,
               color: Color.fromRGBO(_r.toInt(), _g.toInt(), _b.toInt(), 1))),
       Container(constraints: BoxConstraints.expand(height: 32.0)),
       Slider(
           label: "${_size.toInt()} sp",
           value: _size,
           min: 10,
           max: 48,
           divisions: 38,
           activeColor: Colors.black,
           inactiveColor: Colors.grey,
           onChanged: (val) => _setTextStyle(size: val)),
       Slider(
         label: _r.toInt().toString(),
         value: _r,
         min: 0,
         max: 255,
         divisions: 255,
         activeColor: Colors.red,
         inactiveColor: Colors.grey,
         onChanged: (val) => _setTextStyle(r: val),
       ),
       Slider(
         label: _g.toInt().toString(),
         value: _g,
         min: 0,
         max: 255,
         divisions: 255,
         activeColor: Colors.green,
         inactiveColor: Colors.grey,
         onChanged: (val) => _setTextStyle(g: val),
       ),
       Slider(
         label: _b.toInt().toString(),
         value: _b,
         min: 0,
         max: 255,
         divisions: 256,
         activeColor: Colors.blue,
         inactiveColor: Colors.grey,
         onChanged: (val) => _setTextStyle(b: val),
       ),
     ],
   );
 }
}

Так как виджет необходимо обновить (метод _setTextStyle), мы используем StatefulWidget.
Для следующих двух вкладок нет необходимости в обновлении, поэтому будем использовать StatelessWidget.


Вторая вкладка:


class ImageWidget extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return Stack(
     children: <Widget>[
       Center(child: CircularProgressIndicator()),
       Center(
         child: FadeInImage.memoryNetwork(
           placeholder: kTransparentImage,
           image: 'https://picsum.photos/250?image=9',
         ),
       ),
     ],
   );
 }
}

Третья вкладка:


class ListWidget extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return ListView.builder(
     itemCount: 25,
     itemBuilder: (BuildContext context, int index) {
       return Container(
         child: Text(
           'entry $index',
           style: TextStyle(color: Colors.white),
         ),
         margin: EdgeInsets.all(16.0),
         padding: EdgeInsets.all(16.0),
         decoration: BoxDecoration(
           color: Colors.blue,
           borderRadius: BorderRadius.all(
             Radius.circular(16.0),
           ),
         ),
       );
     },
   );
 }
}

Измененный код состояния основного экрана:


class _MyHomePageState extends State<MyHomePage> {
 int _currentIndex = 0;

 Widget build(BuildContext context) {
   return Scaffold(
       appBar: AppBar(
         title: Text(widget.title),
         actions: <Widget>[
           IconButton(icon: Icon(Icons.navigate_next), onPressed: next)
         ],
       ),
       body: <Widget>[
        TextWidget(),
        ImageWidget(),
        ListWidget(),
    ][_currentIndex],
       bottomNavigationBar: BottomNavigationBar(
         currentIndex: _currentIndex,
         onTap: _onTapped,
         items: [
           BottomNavigationBarItem(
             icon: new Icon(Icons.text_format),
             title: new Text('Text'),
           ),
           BottomNavigationBarItem(
             icon: new Icon(Icons.image),
             title: new Text('Image'),
           ),
           BottomNavigationBarItem(
             icon: Icon(Icons.list),
             title: Text('ListView'),
           )
         ],
       ));
 }

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


И все же есть минимальные различия между виджетом, который используется как экран, и обычным виджетом. Корневым элементом для виджета экрана должен быть объект Scaffold (позволяет добавлять appBar, bottomNavigationBar, floatingActionButton, drawer и т.д.).


На обычные виджеты данное ограничение не распространяется, так как с помощью метода build они встроятся в основной экран, где уже есть Scaffold.


К счастью, добавление к обычному виджету Scaffold не сказывается на его работоспособности.


Также можно добавить SafeArea (чтобы обеспечить отступ для status bar). Получается следующее простое преобразование:


От:


@override
Widget build(BuildContext context) {
  return [код];
}

К:


@override
Widget build(BuildContext context) {
  return Scaffold(
    body: SafeArea(
    child: [код]
    ),
  );
}

Ну а теперь вернемся к обсуждению плюсов и минусов Flutter.


Flutter недавно вышел в релиз, поэтому баги встречаются достаточно часто. Особенно это заметно при обновлении Flutter – некоторые библиотеки начинают работать с ошибками.


— Нестабильность (совсем недавно вышел из beta)


Очевидно, что при использовании нового фреймворка в вашем распоряжении гораздо меньше библиотек, чем при нативной разработке по Android/iOS. Однако, библиотек для Flutter все равно довольно много, и они продолжают появляться с большой скоростью. Так, например, многие библиотеки были добавлены во второй половине 2018 года, судя по всему, в рамках подготовки к первому стабильному релизу, а важнейшие библиотеки (Google Analytics, Firebase, Maps и т.д.) существовали и до этого.


— Библиотек меньше, чем для нативной разработки
+ Важнейшие библиотеки уже есть, постоянно выходят новые


Самое время подвести итоги! Давайте вспомним все плюсы и минусы, расположив элементы от самых существенных плюсов до самых существенных минусов:


+ Кроссплатформенность
+ Перспективность и активное развитие
+ Важнейшие библиотеки уже есть, постоянно выходят новые
+ Собственный графический движок
+ Интерфейс легко разбивается на отдельные модули


— Конечный установочный пакет больше, так как в него добавляется виртуальная машина Dart
— Интерфейс создается с помощью кода, из-за чего грань между логикой и дизайном гораздо тоньше
— Библиотек (и информации) меньше, чем для нативной разработки
— Нестабильность (совсем недавно вышел из beta)


Спасибо за внимание!


+15
21.2k 95
Comments 45