Привет, Хабр! Представляем вашему вниманию перевод статьи "Building Layouts".

Сегодня мы узнаем:


  • Как работают механики построения UI на Flutter
  • Как верстать экраны горизонтально и вертикально
  • Как сверстать экран, используя Flutter

Результатом сегодняшнего урока будет следующий сверстанный экран

image


Шаг 0: Настройка проекта


Для начала создадим новый проект File -> New Flutter Project -> next, next, next…
Далее необходимо, создать директорию images в корне проекта и положить туда файл с именем lake.jpg — сам файл скачать можно отсюда — link

Так же необходимо поправить конфигурационный файл pubspec.yaml (что-то вроде gradle для android и cocoa pods в iOS, в нем мы можем добавить внешние зависимости). Сам текст файла можно скачать здесь

Исходник проекта можно скачать здесь — его необходимо поместить в файл main.dart

Шаг 1: Диаграмма экрана


Для начала разобьем макет на простые элементы

  • Определим строки и столбцы
  • Определим включает ли макет сетку?
  • Есть ли перекрывающие элементы
  • Нужны ли вкладки для пользовательского интерфейса?
  • Обратим внимание на области требующие выравнивания или отступы

Сперва определим основные крупные элементы. В этом примере — 4 элемента расположены в виде столбца: картина, две строки и блок теста

image

Далее разберем каждую строку. Первая строка, которая называется «Заголовок» имеет 3 дочерних элемента — столбец текста, иконка звезды и число. Первый столбец содержит 2 строки. Первый столбец занимает много места, поэтому следует обернуть его в расширяемый виджет.

image

Следущий ряд, называемый секцией кнопок, так же имеет 3 дочерних элемента. Каждый из них содержит картинку и текст.

image

Наконец мы разложили макет на простые элементы. Проще всего использовать подход «снизу-вверх» для верстки дисплея. Для того чтобы избегать сложной структуры разбивайте UI на переменные и функции.

Шаг 2: Построим ряд заголовка


Сперва, мы должны построить левый столбец секции заголовка. Вставка столбца внутри расширяемого виджета растягивает столбец для использования всего оставшегося места в ряду. Установим свойство crossAxisAlignment для CrossAxisAlignment.start для выравнивания столбца к началу строки.

Размещение строки текста внутри контейнера позволяет активировать отступы. Второй дочерний элемент в столбце это тоже текст, он отображается серым цветом. Последние два элемента иконка «звезды» нарисована красным цветом и текст со значением «41». Поместим целую строку в контейнер и добавим отступы по 32 пикселя с каждой стороны. Код для выполнения этих действий представлен ниже

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Widget titleSection = Container(
      padding: const EdgeInsets.all(32.0),
      child: Row(
        children: [
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Container(
                  padding: const EdgeInsets.only(bottom: 8.0),
                  child: Text(
                    'Oeschinen Lake Campground',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                Text(
                  'Kandersteg, Switzerland',
                  style: TextStyle(
                    color: Colors.grey[500],
                  ),
                ),
              ],
            ),
          ),
          Icon(
            Icons.star,
            color: Colors.red[500],
          ),
          Text('41'),
        ],
      ),
    );
  //...
}

Шаг 3: Построим ряд кнопок


Секция кнопок состоит из 3 столбцов, которые строятся по похожему принципу — иконка над строкой текста. Столбец в этой строке равномерно заполняется и текст и иконки рисуются основным цветом, который выбран голубым в нашем проекте в методе build().

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //...

    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),

    //...
}

Так как код построения каждого ряда будет практически идентичен, то наиболее эффективно будет использовать вложенную функцию, такую как buildButtonColumn(), которая включает в себя иконку и текст и возвращает столбец с этим виджетом.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //...

    Column buildButtonColumn(IconData icon, String label) {
      Color color = Theme.of(context).primaryColor;

      return Column(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(icon, color: color),
          Container(
            margin: const EdgeInsets.only(top: 8.0),
            child: Text(
              label,
              style: TextStyle(
                fontSize: 12.0,
                fontWeight: FontWeight.w400,
                color: color,
              ),
            ),
          ),
        ],
      );
    }
  //...
}

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //...

    Widget buttonSection = Container(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          buildButtonColumn(Icons.call, 'CALL'),
          buildButtonColumn(Icons.near_me, 'ROUTE'),
          buildButtonColumn(Icons.share, 'SHARE'),
        ],
      ),
    );
  //...
}

Шаг 4: Построим секцию описания


Определим секцию описания, которая довольно длинная. Поместим текст в контейнер и добавим отступы 32 пикселя от каждого края.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //...
    Widget textSection = Container(
      padding: const EdgeInsets.all(32.0),
      child: Text(
        '''
Lake Oeschinen lies at the foot of the Bluemlisalp in the Bernese Alps. Situated 1,578 meters above sea level, it is one of the larger Alpine Lakes. A gondola ride from Kandersteg, followed by a half-hour walk through pastures and pine forest, leads you to the lake, which warms to 20 degrees Celsius in the summer. Activities enjoyed here include rowing, and riding the summer toboggan run.
        ''',
        softWrap: true,
      ),
    );
  //...
}

Шаг 5: Построим секцию с изображением


Три из четырех столбцов уже построены, осталось сделать только только столбец изображения. Изображение, которе используется в данном проекте находится доступно онлайн под лицензией «Creative Commons license». Но оно большое и загружаться оно будет медленно. В шаге 0 мы добавили изображение к нашему проекту и обновили конфигурационный файл, теперь добавим ссылку на него в своем коде.

return MaterialApp(
//...
body: ListView(
  children: [
    Image.asset(
      'images/lake.jpg',
      height: 240.0,
      fit: BoxFit.cover,
    ),
    // ...
  ],
),
//...
);

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

Шаг 6: Объединим все вместе


В финальном шаге соберем все кусочки нашего кода вместе. Виджет организован в ListView, а не Column потому ListView автоматически скроллится во время прокрутки на маленьком устройстве.

//...
return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Top Lakes'),
        ),
        body: ListView(
          children: [
            Image.asset(
              'images/lake.jpg',
              width: 600.0,
              height: 240.0,
              fit: BoxFit.cover,
            ),
            titleSection,
            buttonSection,
            textSection,
          ],
        ),
      ),
    );
//...

Комментарии (17)


  1. LionisIAm
    15.12.2018 19:10

    Не хватает картинок/скриншота результатов. Флаттер, как по мне, по прежнему не столь популярен, чтобы можно было превращать код в картинку в голове.


    1. Tarik02
      15.12.2018 19:58

      Присоединяюсь, картинки там есть, но они загружены в ВК и в Украине не грузятся. Просьба автору загружать картинки на habrastorage.


      1. shanlove
        15.12.2018 20:37
        +3

        Оно и из России ломается, что интересно)


      1. namikiri
        16.12.2018 01:38

        Похоже, ВК автоматически выдаёт волшенбное ничего, если видит Referer хабра.

        image, image, image


      1. ermolnik Автор
        16.12.2018 10:09

        Спасибо, поправил


  1. EnChikiben
    15.12.2018 19:22

    Сырой он, сырой, из релиза в релиз что нибудь да ломается… про Dart я вообще молчу баги весят годами…


    1. unnutz
      16.12.2018 00:14

      Но ведь был всего один релиз (он же первый)…


  1. HeaTTheatR
    15.12.2018 21:18
    +1

    Пощупал. Погонял. Шустренько, да, но писать на этом невозможно! Оно не читабельно уже через два часа после написания этих трехэтажных конструкций.


    1. basnopisets
      17.12.2018 13:59

      глядишь, лет через 5 народ сформулирует best practices


    1. bagzon
      17.12.2018 14:39

      ну так надо разделять на компоненты, и выносить, подключая импортом выходит все гармонично)

      А если писать все в одном файле то да, не очень как то.


      1. HeaTTheatR
        17.12.2018 16:14

        Все равно, строить UI прямо в коде — не хорошая идея. Не будешь же ты каждую кнопку и каждую подпись выносить в отдельный файл. Если бы у Flutter был бы какой-нибудь декларативный язык описания UI, цены ему не было. А так — все это выглядит грязно.


        1. Neikist
          17.12.2018 16:36

          Дело вкуса. Имхо, это выглядит нагляднее и читабельнее андроидных xml портянок, особенно если придерживаться минимальных правил.


          1. HeaTTheatR
            17.12.2018 17:27

            Зато в этих портянках разберется практически любой программист, даже если он впервые их видит.


            1. Neikist
              17.12.2018 18:27

              Ну я почти одинаковое время разбирался с нативным андроидом и флаттером, и флаттер для чтения мне был проще.


        1. unnutz
          18.12.2018 11:01

          Вот здесь (миф номер 3) есть сравнение подходов. Согласен, дело вкуса, вот только не теряется время на парсинг еще одного файла шаблонов.


  1. Dair_Targ
    16.12.2018 16:25

    Я правильно понимаю, что основное преимущество перед React — то что Flutter сделан в Google?


    1. sergeyfitis
      16.12.2018 23:47

      Нет, он работает иначе. У него свой рендер UI