Всем доброго дня!

Меня зовут Алексей, я основатель и frontend разработчик системы автоматизации работы управляющих компаний «Оператор 18». 

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

Как то я уже писал статью об Операторе18. Но тогда это была первая версия проекта, я собирал мнения, пробовал оценить рынок, выбрать вариант монетизации и т. д.

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

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


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

Для второй версии я решил заказать дизайн.

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

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

Изначально я был уверен что буду сам писать виджеты, прямо с нуля. Я считал что не стоит держать в проекте много сторонних зависимостей! Можешь если сам написать что-то — пиши! 

Вот, например, окно входа в систему в новом дизайне выглядит так:

И я, решив прислушаться к себе, не стал искать готовых решений, а написал свой виджет:

class LoginDropdown extends StatefulWidget {
  final Function(String) onUserRoleChanged;

  const LoginDropdown({
    required this.onUserRoleChanged,
  });

  @override
  State<LoginDropdown> createState() => _LoginDropdownState();
}

class _LoginDropdownState extends State<LoginDropdown> {
  bool isShowMenu = false;
  String currentRole = UserRole.mcOperator;
  Color roleDropdownButtonColor = AppColors.gray_3;

  @override
  Widget build(
    BuildContext context,
  ) =>
      MouseRegion(
        onEnter: (_) =>
            setState(() => roleDropdownButtonColor = AppColors.gray_1),
        onExit: (_) =>
            setState(() => roleDropdownButtonColor = AppColors.gray_3),
        child: GestureDetector(
          onTap: () {
            setState(() {
              // ignore: avoid_bool_literals_in_conditional_expressions
              isShowMenu = isShowMenu ? false : true;
            });
          },
          child: Stack(
            children: [
              Container(
                height: 56,
                width: 418,
                decoration: BoxDecoration(
                  color: roleDropdownButtonColor,
                  borderRadius: const BorderRadius.all(
                    Radius.circular(12),
                  ),
                ),
                child: Padding(
                  padding: const EdgeInsets.only(
                    left: 20,
                    right: 20,
                  ),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(
                        currentRole,
                        style: AppFonsts.dropDown_1,
                      ),
                      Image.asset(
                        'icons/dropdown_arrow.png',
                        height: 20,
                        width: 20,
                      ),
                    ],
                  ),
                ),
              ),
              if (isShowMenu)
                Padding(
                  // 56 - is height of first container,
                  // 8 - is constraint between containers
                  padding: const EdgeInsets.only(top: 56 + 8),
                  child: Container(
                    height: 166,
                    width: 418,
                    decoration: const BoxDecoration(
                      boxShadow: [
                        // BoxShadow setup found here:
                        // https://devsheet.com/code-snippet/add-box-shadow-to-container-in-flutter/
                        BoxShadow(
                          color: AppColors.gray_6,
                          blurRadius: 90,
                          offset: Offset(0, 20),
                        )
                      ],
                      color: AppColors.white,
                      borderRadius: BorderRadius.all(
                        Radius.circular(12),
                      ),
                    ),
                    child: Padding(
                      padding: const EdgeInsets.symmetric(vertical: 14),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          for (var role in UserRole.list)
                            LoginDropdownItem(
                              onTap: () {
                                setState(() {
                                  isShowMenu = false;
                                  currentRole = role;
                                  widget.onUserRoleChanged(role);
                                });
                              },
                              userRole: role,
                            ),
                        ],
                      ),
                    ),
                  ),
                ),
            ],
          ),
        ),
      );
}

Для меня «свой виджет» — это не просто кнопка, например, которой поменяли цвет и скруглили углы. «Свой виджет» — это виджет при создании которого нужно описывать его поведение и реакцию на действия пользователя. 

Получилось хорошо, возможно не хватает какой нибудь анимации, но в целом — я остался доволен!

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

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

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

// Inside a Row widget
DropdownButtonHideUnderline(
   child: DropdownButton2(
      icon: const SizedBox(),
      dropdownWidth: 418.w,
      dropdownMaxHeight: 233.h,
      dropdownDecoration: BoxDecoration(
         borderRadius: BorderRadius.circular(16.r),
      ),
      hint: Row(
         children: [
            Text(
               AppString.more,
               style: AppFonts.menuUnselected,
            ),
            SizedBox(
               width: 5.w,
            ),
            Image.asset(
               'icons/dropdown_arrow.png',
               height: 20.h,
               width: 20.w,
            ),
         ],
      ),
      items: items.map(
         (item) => DropdownMenuItem<String>(
            value: item,
            child: Text(
               item,
               style: AppFonts.dropDownBlack,
            ),
         ),
      ).toList(),
      value: selectedValue,
      onChanged: widget.onLogSelected,
      itemHeight: 35.h,
      itemPadding: EdgeInsets.only(
         left: 28.w,
      ), 
   ),
),
// Inside a Row widget

Спасибо за прочтение! Буду благодарен за критику/советы/иные комментарии.

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


  1. Haid00k
    31.08.2022 20:12
    +2

    Интересно, пишите ещё.

    Вы самостоятельно пишете «с нуля» всю CRM?


    1. kharitonovAL Автор
      31.08.2022 20:16

      Добрый день! Спасибо за ваш комментарий.

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


  1. ookami_kb
    31.08.2022 20:25

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

    А откуда возьмется опыт, если не создавать свои виджеты? Понятно, что если опыта нет, то будет долго и плохо. Например, конкретно в этом случае можно было взять стандартный DrodownButton и кастомизовать его, а не изобретать странный велосипед с MouseRegion и Stack.

    А уж тащить ради этого левый пакет, ну это уже какой-то npm-синдром.

    Вся статья, это вопрос – писать виджет самому или искать готовый пакет? Ну тогда ответ: it depends. Но в 90% случаев, лучше писать самому. Сначала – для обучения, потом – чтобы не скатываться в dependency hell там, где это совершенно не нужно.


    1. kharitonovAL Автор
      31.08.2022 20:54

      Спасибо за комментарий!

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

      MouseRegion нужен для изменения цвета фона строки над которым находится указатель, ведь весь UI конкретного модуля - исключительно для web application. Хотя, теперь можно наверно и в нативку винды завернуть.

      Stack нужен для того, чтобы элементы не сдвигались. Если взять Column, то при нажатии на выпадающий элемент остальные просто сдвинутся вниз на соответствующую высоту. А Stack как раз позволяет сделать этакий overlap.

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


      1. ookami_kb
        31.08.2022 22:05
        +1

        Stack нужен для того, чтобы элементы не сдвигались. Если взять Column, то при нажатии на выпадающий элемент остальные просто сдвинутся вниз на соответствующую высоту. А Stack как раз позволяет сделать этакий overlap.

        Для этого в том же DropdownButton используется Overlay, он даст ряд преимуществ.

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

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

        Опять же, говоря о приложении в целом, одно дело, если вы берете готовый UI-kit с возможностью кастомизации, и используете его (почти) целиком (например, https://pub.dev/packages/macos_ui) – тут подключение библиотеки оправдано. А другое дело, когда на каждый компонент будет отдельный пакет, и потом все это кастомизовать и поддерживать... Флаттер – это уже, в первую очередь, UI-фреймворк, причем с довольно хорошо продуманным API и хорошей библиотекой стандартных компонентов. Советую больше времени потратить на изучение самого фреймворка и стандартных виджетов, чем на поиск и кастомизацию сторонних пакетов.


        1. kharitonovAL Автор
          01.09.2022 05:04

          Я в вообще и сам не приветствую множество сторонних зависимостей. Большое спасибо за ценный совет! Буду изучать!