0 added
0 removed
Original
2026-01-01
Modified
2026-03-10
1
<ul><li><a>Цели проектной работы</a></li>
1
<ul><li><a>Цели проектной работы</a></li>
2
<li><a>Используемые технологии</a></li>
2
<li><a>Используемые технологии</a></li>
3
<li><a>Реализация</a></li>
3
<li><a>Реализация</a></li>
4
<li><a>Диаграмма классов</a></li>
4
<li><a>Диаграмма классов</a></li>
5
<li><a>Итоги</a></li>
5
<li><a>Итоги</a></li>
6
</ul><p><em>Автор проекта -<strong>Наталья Куликова</strong>.</em></p>
6
</ul><p><em>Автор проекта -<strong>Наталья Куликова</strong>.</em></p>
7
<p>Я работаю над мобильным приложением, в котором есть лента новостей. Для разметки тела новости используются контент-блоки, которые поставляются бекендом. Контент-блоки - это блоки с произвольным содержимым, по типу блока определяется его вид и расположение при отображении. Ранее в приложении была реализована работа с контент-блоками через рекурсивный парсинг исходного json. При добавлении новых контент-блоков каждый раз приходилось переписывать модуль, а в теле метода, отвечающего за разбор json, были перемешаны логика разбора и классы разметки. При очередном добавлении нового контент-блока мне захотелось это изменить.</p>
7
<p>Я работаю над мобильным приложением, в котором есть лента новостей. Для разметки тела новости используются контент-блоки, которые поставляются бекендом. Контент-блоки - это блоки с произвольным содержимым, по типу блока определяется его вид и расположение при отображении. Ранее в приложении была реализована работа с контент-блоками через рекурсивный парсинг исходного json. При добавлении новых контент-блоков каждый раз приходилось переписывать модуль, а в теле метода, отвечающего за разбор json, были перемешаны логика разбора и классы разметки. При очередном добавлении нового контент-блока мне захотелось это изменить.</p>
8
<h2>Цели проектной работы</h2>
8
<h2>Цели проектной работы</h2>
9
<ol><li>Вынести работу с парсером контент-блоков в отдельную библиотеку для возможности переиспользования в других проектах.</li>
9
<ol><li>Вынести работу с парсером контент-блоков в отдельную библиотеку для возможности переиспользования в других проектах.</li>
10
<li>Упростить добавление новых контент-блоков.</li>
10
<li>Упростить добавление новых контент-блоков.</li>
11
<li>Отделить описание отображения контент-блоков от алгоритма разбора структуры json-документа.</li>
11
<li>Отделить описание отображения контент-блоков от алгоритма разбора структуры json-документа.</li>
12
<li>Использовать<a>знания, полученные на курсе</a>.</li>
12
<li>Использовать<a>знания, полученные на курсе</a>.</li>
13
</ol><h2>Используемые технологии</h2>
13
</ol><h2>Используемые технологии</h2>
14
<ol><li><strong>Dart + Flutter</strong>для приложения.</li>
14
<ol><li><strong>Dart + Flutter</strong>для приложения.</li>
15
<li><strong>GetIt</strong>в качестве IoC-контейнера. GetIt - достаточно простой простой и удобный service locator для Dart и Flutter, часто обновляется и поддерживается.</li>
15
<li><strong>GetIt</strong>в качестве IoC-контейнера. GetIt - достаточно простой простой и удобный service locator для Dart и Flutter, часто обновляется и поддерживается.</li>
16
<li>Шаблон<strong>“Visitor (Посетитель)”</strong>в качестве архитектурного решения. Хорошо подходит для написания парсеров. Применяется там, где нужно сделать ряд операций над объектами разных типов, но нужно избежать загрязнения их кода.</li>
16
<li>Шаблон<strong>“Visitor (Посетитель)”</strong>в качестве архитектурного решения. Хорошо подходит для написания парсеров. Применяется там, где нужно сделать ряд операций над объектами разных типов, но нужно избежать загрязнения их кода.</li>
17
</ol><h2>Реализация</h2>
17
</ol><h2>Реализация</h2>
18
<p>Для начала выделим базовые компоненты для построения структуры классов.</p>
18
<p>Для начала выделим базовые компоненты для построения структуры классов.</p>
19
<p>Базовый абстрактный класс для контент-блока:</p>
19
<p>Базовый абстрактный класс для контент-блока:</p>
20
abstract class IBlock { //тип блока final String type; final String id; //объект со всеми параметрами блока final Map<String, dynamic>? obj; IBlock(this.type, this.id, {this.obj}); //стандартный метод из шаблона "Visitor" void accept(Visitor visitor) => visitor.visitElement(this); }<p>Сделаем реализацию блоков, соответствующих данному абстрактному классу.</p>
20
abstract class IBlock { //тип блока final String type; final String id; //объект со всеми параметрами блока final Map<String, dynamic>? obj; IBlock(this.type, this.id, {this.obj}); //стандартный метод из шаблона "Visitor" void accept(Visitor visitor) => visitor.visitElement(this); }<p>Сделаем реализацию блоков, соответствующих данному абстрактному классу.</p>
21
<p>Первый класс - это простейший блок, в нем реализуем парсинг элемента из json:</p>
21
<p>Первый класс - это простейший блок, в нем реализуем парсинг элемента из json:</p>
22
class ContentBlock extends IBlock { ContentBlock(super.type, super.id, {required super.obj}); static IBlock fromJson(Map<String, dynamic> obj) { if (obj.containsKey('items')) { return BlockContainer( obj['type'], obj['id'], obj: Map.from(obj), children: (obj['items'] as List<Map<String, dynamic>>) .map((e) => ContentBlock.fromJson(e)) .toList(), ); } return ContentBlock( obj['type'], obj['id'], obj: Map.from(obj), ); } }<p>Второй класс - это контейнер, содержащий дочерние блоки. В нем добавим поле для списка дочерних элементов, плюс переопределим метод accept, чтобы проходить по всем дочерним элементам:</p>
22
class ContentBlock extends IBlock { ContentBlock(super.type, super.id, {required super.obj}); static IBlock fromJson(Map<String, dynamic> obj) { if (obj.containsKey('items')) { return BlockContainer( obj['type'], obj['id'], obj: Map.from(obj), children: (obj['items'] as List<Map<String, dynamic>>) .map((e) => ContentBlock.fromJson(e)) .toList(), ); } return ContentBlock( obj['type'], obj['id'], obj: Map.from(obj), ); } }<p>Второй класс - это контейнер, содержащий дочерние блоки. В нем добавим поле для списка дочерних элементов, плюс переопределим метод accept, чтобы проходить по всем дочерним элементам:</p>
23
class BlockContainer extends IBlock { final List<IBlock> children; BlockContainer(super.type, super.id, {super.obj, required this.children}); @override void accept(Visitor visitor) { visitor.beforeVisit(this); for (final child in children) { child.accept(visitor); } visitor.visitContainer(this); visitor.afterVisit(this); } }<p>Добавим методы beforeVisit , visitContainer и afterVisit для того, чтобы определять количество и порядковый номер элемента в контейнере. Это нам пригодится для отрисовки сложных контент-блоков, например, нумерованных списков или для изменения стилей последнего элемента в блоке.</p>
23
class BlockContainer extends IBlock { final List<IBlock> children; BlockContainer(super.type, super.id, {super.obj, required this.children}); @override void accept(Visitor visitor) { visitor.beforeVisit(this); for (final child in children) { child.accept(visitor); } visitor.visitContainer(this); visitor.afterVisit(this); } }<p>Добавим методы beforeVisit , visitContainer и afterVisit для того, чтобы определять количество и порядковый номер элемента в контейнере. Это нам пригодится для отрисовки сложных контент-блоков, например, нумерованных списков или для изменения стилей последнего элемента в блоке.</p>
24
<p>Получим такой интерфейс для самого визитора:</p>
24
<p>Получим такой интерфейс для самого визитора:</p>
25
abstract class Visitor { void visitElement(IBlock element); void visitContainer(BlockContainer container); void beforeVisit(BlockContainer container); void afterVisit(BlockContainer container); }<p>Дальше рассмотрим реализацию визитора для обхода блоков с целью превратить их в отображаемые виджеты.</p>
25
abstract class Visitor { void visitElement(IBlock element); void visitContainer(BlockContainer container); void beforeVisit(BlockContainer container); void afterVisit(BlockContainer container); }<p>Дальше рассмотрим реализацию визитора для обхода блоков с целью превратить их в отображаемые виджеты.</p>
26
<p>Полностью код визитора выглядит так:</p>
26
<p>Полностью код визитора выглядит так:</p>
27
class WidgetCreatorVisitor implements Visitor { final Queue<List<dynamic>> result = Queue()..add([]); final Queue<BlockContainer> parent = Queue(); @override void beforeVisit(BlockContainer element) { result.add([]); element.obj?['index'] = 0; element.obj?['parentSize'] = element.children.length; parent.add(element); } @override void afterVisit(BlockContainer element) { parent.removeLast(); } @override void visitElement(IBlock element) { _setFromParent(element, parent.last); result.last.add(getBlockWidget(element, parent: parent.last)); } @override void visitContainer(BlockContainer container) { final children = List<dynamic>.from(result.last); final containerParent = (parent.length-2 >= 0 ) ? parent.elementAt(parent.length-2) : null; _setFromParent(container, containerParent); result.removeLast(); result.last.add(getBlockWidget( container, children: children, parent: containerParent )); } void _setFromParent(IBlock element, BlockContainer? parent) { if (parent == null) return; element.obj?['index'] = parent.obj?['index']; parent.obj?['index'] ++; element.obj?['parentSize'] = parent.obj?['parentSize']; } }<p>Создадим очередь, в которую будем складывать получившиеся виджеты (<em>Queue<List<dynamic>> result</em>) и очередь для временного хранения родительских блоков (<em>Queue<BlockContainer> parent</em>), чтобы можно было обогащать дочерние блоки информацией о родителе (например о нумерации).</p>
27
class WidgetCreatorVisitor implements Visitor { final Queue<List<dynamic>> result = Queue()..add([]); final Queue<BlockContainer> parent = Queue(); @override void beforeVisit(BlockContainer element) { result.add([]); element.obj?['index'] = 0; element.obj?['parentSize'] = element.children.length; parent.add(element); } @override void afterVisit(BlockContainer element) { parent.removeLast(); } @override void visitElement(IBlock element) { _setFromParent(element, parent.last); result.last.add(getBlockWidget(element, parent: parent.last)); } @override void visitContainer(BlockContainer container) { final children = List<dynamic>.from(result.last); final containerParent = (parent.length-2 >= 0 ) ? parent.elementAt(parent.length-2) : null; _setFromParent(container, containerParent); result.removeLast(); result.last.add(getBlockWidget( container, children: children, parent: containerParent )); } void _setFromParent(IBlock element, BlockContainer? parent) { if (parent == null) return; element.obj?['index'] = parent.obj?['index']; parent.obj?['index'] ++; element.obj?['parentSize'] = parent.obj?['parentSize']; } }<p>Создадим очередь, в которую будем складывать получившиеся виджеты (<em>Queue<List<dynamic>> result</em>) и очередь для временного хранения родительских блоков (<em>Queue<BlockContainer> parent</em>), чтобы можно было обогащать дочерние блоки информацией о родителе (например о нумерации).</p>
28
<p>Описание методов визитора:</p>
28
<p>Описание методов визитора:</p>
29
<p><strong>beforeVisit</strong>- сохраняем сам блок в очередь и добавляем в него информацию для внутренних блоков перед началом обхода элементов блока.</p>
29
<p><strong>beforeVisit</strong>- сохраняем сам блок в очередь и добавляем в него информацию для внутренних блоков перед началом обхода элементов блока.</p>
30
<p><strong>afterVisit</strong>- удаляем блок из очереди после окончания обхода.</p>
30
<p><strong>afterVisit</strong>- удаляем блок из очереди после окончания обхода.</p>
31
<p><strong>visitElement</strong>- метод для простых блоков. Обогащаем элемент данными из родительского блока, затем по типу блока достаем из IoC-контейнера виджет и складываем в очередь результата.</p>
31
<p><strong>visitElement</strong>- метод для простых блоков. Обогащаем элемент данными из родительского блока, затем по типу блока достаем из IoC-контейнера виджет и складываем в очередь результата.</p>
32
<p><strong>visitContainer</strong>- метод для контейнеров. Так же обогащаем элемент данными из родительского блока (берем из очереди<em>parent</em>предпоследний элемент, т. к. последний - это сам текущий контейнер), по типу блока достаем из IoC-контейнера виджет и в качестве дочерних элементов передаем ему все, что было в результирующей очереди. Очищаем очередь и кладем туда только этот виджет-контейнер.</p>
32
<p><strong>visitContainer</strong>- метод для контейнеров. Так же обогащаем элемент данными из родительского блока (берем из очереди<em>parent</em>предпоследний элемент, т. к. последний - это сам текущий контейнер), по типу блока достаем из IoC-контейнера виджет и в качестве дочерних элементов передаем ему все, что было в результирующей очереди. Очищаем очередь и кладем туда только этот виджет-контейнер.</p>
33
<p>Основная часть готова. Теперь допишем метод для регистрации и выборки виджетов по типу блоков в IoC-контейнере - и можно внедрять в проект:</p>
33
<p>Основная часть готова. Теперь допишем метод для регистрации и выборки виджетов по типу блоков в IoC-контейнере - и можно внедрять в проект:</p>
34
class ContentBlocks extends StatelessWidget { ContentBlocks({super.key}); final _creator = WidgetCreatorVisitor(); final _blocks = ContentBlock.fromJson(jsonContentBlocks); @override Widget build(BuildContext context) { _blocks.accept(_creator); return SingleChildScrollView(child: _creator.get()); } }<h2>Диаграмма классов</h2>
34
class ContentBlocks extends StatelessWidget { ContentBlocks({super.key}); final _creator = WidgetCreatorVisitor(); final _blocks = ContentBlock.fromJson(jsonContentBlocks); @override Widget build(BuildContext context) { _blocks.accept(_creator); return SingleChildScrollView(child: _creator.get()); } }<h2>Диаграмма классов</h2>
35
<a></a><h2>Итоги</h2>
35
<a></a><h2>Итоги</h2>
36
<p>Результат получился таким, как я задумывала. Теперь при появлении контент-блоков новых типов можно сосредоточить свое внимание на написании виджетов. Достаточно создать виджет, которому в качестве параметров передать ContentBlock. Если виджет сложный, со многими параметрами, то можно написать Adapter, который будет превращать ContentBlock в требуемые параметры для виджета. Созданный виджет необходимо зарегистрировать в IoC-контейнере через метод библиотеки.</p>
36
<p>Результат получился таким, как я задумывала. Теперь при появлении контент-блоков новых типов можно сосредоточить свое внимание на написании виджетов. Достаточно создать виджет, которому в качестве параметров передать ContentBlock. Если виджет сложный, со многими параметрами, то можно написать Adapter, который будет превращать ContentBlock в требуемые параметры для виджета. Созданный виджет необходимо зарегистрировать в IoC-контейнере через метод библиотеки.</p>
37
<p>С реализованной библиотекой и примером использования можно ознакомиться здесь:<a>https://github.com/hoxa-k/flutter_content_block_parser</a></p>
37
<p>С реализованной библиотекой и примером использования можно ознакомиться здесь:<a>https://github.com/hoxa-k/flutter_content_block_parser</a></p>
38
<p>P. S. <em>Интересуюет архитектура ПО и шаблоны проектирования? Добро пожаловать на <a>специализированный курс в Otus</a>!</em></p>
38
<p>P. S. <em>Интересуюет архитектура ПО и шаблоны проектирования? Добро пожаловать на <a>специализированный курс в Otus</a>!</em></p>
39
39