Введение

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

Экран содержит:

  • Список пар полей «Имя» и «Возраст» создается динамически (первые три пары создаются программно).
  • Кнопка для каждой пары, чтобы удалить их из списка.
  • Кнопка для добавления дополнительных пар полей.
  • Кнопка отправки.

Часть 1. Настройка проекта

1 - Создайте новый проект флаттера:

flutter create your_project_name

2 - Отредактируйте файл pubspec.yaml и добавьте пакеты frideos:

dependencies:
  flutter:
    sdk: flutter 
  frideos: ^0.6.1

3 - Удалите содержимое файла main.dart и напишите следующее:

import ‘package:flutter/material.dart’;
import ‘dynamic_fields.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: DynamicFieldsPage(),
    );
  }
}

Это будет точка входа в пример, а DynamicFieldsPage виджет, содержащий основные виджеты.

В примере есть еще два файла:

  • bloc.dart: файл класса BLoC. В этом случае для простоты он объявлен глобальным экземпляром этого класса.
  • dynamic_field.dart: он содержит класс DynamicFieldsPage (это StatefulWidget), FieldsWidget (Stateless) и DynamicFieldsWidget (Stateless).

Часть 2 - класс BLoC

Свойства

Для обработки всех полей объявляется StreamedList из StreamedValue<String> для каждого из двух типов полей, имени и возраста. Это необходимо для того, чтобы можно было добавлять новые поля, связанные с потоком, и обрабатывать их проверку (см. Метод checkForm).

final nameFields = StreamedList<StreamedValue<String>>(initialData: []);
final ageFields = StreamedList<StreamedValue<String>>(initialData: []);

Свойство isFormValid (aStramedValue<bool>) используется для потоковой передачи текущего состояния формы. Это будет использоваться для управления виджетом StreamBuilder в DynamicFieldsWidget (объяснено в следующем шаге), чтобы включить или отключить кнопку отправки.

final isFormValid = StreamedValue<bool>();

Конструктор

В конструкторе оба StreamedList инициализируются тремя StreamedValue<String> каждый, так что приложение запускается с тремя парами полей имени / возраста, заполненными некоторыми значениями.

Затем методу onChange каждого StreamedValue назначается метод checkForm, чтобы запускать проверку всех полей каждый раз, когда изменяется одно поле.

DynamicFieldsBloc() {
  print(‘ — — — -DynamicFields BLOC — — — — ‘);
  // Adding the initial three pairs of fields to the screen
  nameFields.addAll([
    StreamedValue<String>(initialData: ‘Name AA’),
    StreamedValue<String>(initialData: ‘Name BB’),
    StreamedValue<String>(initialData: ‘Name CC’)
  ]);
  ageFields.addAll([
    StreamedValue<String>(initialData: ‘11’),
    StreamedValue<String>(initialData: ‘22’),
    StreamedValue<String>(initialData: ‘33’)
  ]);
  // Set the method to call every time the stream emits a new event
  for (var item in nameFields.value) {
    item.onChange(checkForm);
  }
  for (var item in ageFields.value) {
    item.onChange(checkForm);
  }  
}

checkForm

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

Для логических переменных isValidFieldsTypeName и isValidFieldsTypeAge по умолчанию установлено значение true: соответствующее логическое значение устанавливается в значение false, если поле этого типа (имя или возраст) недопустимо.

В последних строках проверяется, истинны ли оба этих логических значения: это означает, что все поля действительны. В этом случае для isFormValid (a StreamedType<bool>) установлено значение true, в поток отправляется истинное событие, запускающее StreamBuilder для восстановления виджета с новым значением, активируя кнопку отправки. В противном случае, если хотя бы одно поле недействительно, условие if не выполняется и isFormValid устанавливается в значение false (кнопка отправки будет отключена).

newFields

Этот метод используется для добавления на экран новой пары полей имени / возраста. В первых двух строках новый StreamedValue<String> добавляется к nameFields и ageFields StreamedList, а егоonChange методу назначается checkFormmethod, как показано в конструкторе. Наконец, вызывается метод thecheckForm для обнаружения новых полей, пустое значение которых приведет к отключению кнопки отправки.

void newFields() {
  nameFields.addElement(StreamedValue<String>();
  ageFields.addElement(StreamedValue<String>();
  
  nameFields.value.last.onChange(checkForm);
  ageFields.value.last.onChange(checkForm);
  // This is used to force the checking of the form so that, adding
  // the new fields, it can reveal them as empty and sets the form
  // to not valid.
  checkForm(null);
}

Часть 3 - интерфейс

DynamicFieldsPage

В методе build класса State, связанного с этим StatefulWidget, возвращается Scaffold с простым AppBar и DynamicFieldsWidget в его теле. В dispose методе этого виджета вызывается dispose метод экземпляра блока для закрытия всех открытых потоков.

class DynamicFieldsPage extends StatefulWidget {
  @override_DynamicFieldsPageState createState() => 
   DynamicFieldsPageState();
}
class _DynamicFieldsPageState extends State<DynamicFieldsPage> {
  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          title: const Text(‘Dynamic fields validation’),
        ),
        body: DynamicFieldsWidget(),
      ),
    );
  }
}

DynamicFieldsWidget

Это виджет, содержащий поля. В методе build он возвращает ListView, так что можно добавить неопределенное количество полей. Внутри ListView есть виджет ValueBuilder, который будет перестраивать виджеты в своем построителе каждый раз, когда поток генерирует новое событие. Внутри построителя есть виджет Column, а его дочерние элементы создаются методом _buildFields.

Кроме того, для кнопок используется виджет Row: один для добавления новых полей, а другой - для отправки формы.

_buildFields (длина целого числа)

Этот метод принимает в качестве параметра длину списка полей имени и строит список полей.

В первой части очищается список TextEditingController:

nameFieldsController.clear();
ageFieldsController.clear();

Затем с помощью цикла for добавляется контроллер для каждого поля. Это будет использоваться для установки начального текста поля. Значения имен и возрастов берутся из StreamedValue типа String, объявленного в классе BLoC.

for (int i = 0; i < length; i++) {
  final name = bloc.nameFields.value[i].value;
  final age = bloc.ageFields.value[i].value;
  nameFieldsController.add(TextEditingController(text: name));
  ageFieldsController.add(TextEditingController(text: age));
}

Наконец, создается список из FieldsWidget:

return List<Widget>.generate(
  length,
  (i) => FieldsWidget(
      index: i,
      nameController: nameFieldsController[i],
      ageController: ageFieldsController[i],
     ),
);

FieldsWidget

Этот виджет принимает в качестве параметра индекс и два TextEditingController и строит каждую пару полей имени / возраста, связанных с кнопкой для их удаления.

Индекс используется для перехода к StreamBuilder каждого TextField соответствующегоStreamedValue.

StreamBuilder<String>(
    initialData: ‘ ‘,
    stream: bloc.nameFields.value[index].outStream,
    builder: (context, snapshot) {
     return Column(
       children: <Widget>[
         Padding(
           padding: const EdgeInsets.symmetric(
             horizontal: 10,
           ),
           child: TextField(
             controller: nameController,
             style: const TextStyle(
               fontSize: 14,
               color: Colors.black,
             ),
             decoration: InputDecoration(
               labelText: ‘Name:’,
               hintText: ‘Insert a name…’,
               errorText: snapshot.error,
             ),
             onChanged: bloc.nameFields.value[index].inStream,
            ),
          ),
        ],
     );
}),

Давайте проанализируем этот фрагмент в его наиболее важных частях:

Поток:

В этом случае с помощью параметра index он передается в параметр потока StreamBuilder единственного элемента StreamedList nameFields (список StreamedValue<String>) для создания текущего поля имени.

stream: bloc.nameFields.value[index].outStream,
  • nameFields - это StreamedListof StreamedValue<String>
  • nameFields.value - это само List, а nameFields.value [индекс] - это одиночный StreamedValue<String>.
  • outStream - получатель потока одиночного StreamedValue<String>

onChanged:

В параметр onChanged передается установщик inStream того же StreamedValue<String>, чтобы отправлять в поток каждое изменение в этом поле.

onChanged: bloc.nameFields.value[index].inStream,

Заключение

Это всего лишь метод проверки динамически создаваемых полей с потоками и шаблоном BLoC довольно простым способом. Как всегда, не стесняйтесь комментировать любые предложения, советы и т. Д. До встречи в следующей статье :-) А пока вы можете найти исходный код этого примера в этом репозитории GitHub.