<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://dartway.dev/ru/blog</id>
    <title>Блог DartWay</title>
    <updated>2026-09-06T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://dartway.dev/ru/blog"/>
    <subtitle>Full-stack Dart в проде: инженерные разборы, кейсы, релизы фреймворка.</subtitle>
    <icon>https://dartway.dev/ru/favicon.ico</icon>
    <rights>Copyright © 2026 DartWay.</rights>
    <entry>
        <title type="html"><![CDATA[Flutter 3.47: что ломается на самом деле и что сделать до ноября]]></title>
        <id>https://dartway.dev/ru/blog/flutter-3-47-what-breaks</id>
        <link href="https://dartway.dev/ru/blog/flutter-3-47-what-breaks"/>
        <updated>2026-09-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[material и cupertino уехали из SDK. Сегодня ничего не ломается, миграция — это одна команда с двумя ловушками, а самое часто повторяемое утверждение про этот релиз — что ChangeNotifier переехал — пока неверно.]]></summary>
        <content type="html"><![CDATA[
<p>За лето вышел ровно один стабильный релиз Flutter. 3.45 и 3.46 до stable вообще
не добрались, а 3.47 приехал 12 августа с 1358 коммитами поверх 3.44. Главное в
нём — не фича. Это переупаковка: <strong><code>material</code> и <code>cupertino</code> уехали из SDK в
отдельные пакеты.</strong></p>
<p>Звучит тревожно, но по большей части это не так — сегодня. Вот что мы выяснили,
мигрируя реальный проект с 3.44.0 на 3.47.2, и одно широко разошедшееся
утверждение про этот релиз, которое не выдерживает столкновения с кодом.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="сегодня-ничего-не-горит">Сегодня ничего не горит<a href="https://dartway.dev/ru/blog/flutter-3-47-what-breaks#%D1%81%D0%B5%D0%B3%D0%BE%D0%B4%D0%BD%D1%8F-%D0%BD%D0%B8%D1%87%D0%B5%D0%B3%D0%BE-%D0%BD%D0%B5-%D0%B3%D0%BE%D1%80%D0%B8%D1%82" class="hash-link" aria-label="Прямая ссылка на Сегодня ничего не горит" title="Прямая ссылка на Сегодня ничего не горит" translate="no">​</a></h2>
<p>Мы взяли живой проект, перевели его с 3.44.0 на 3.47.2 и запустили: <code>flutter analyze</code> чистый, тесты зелёные, библиотеки по-прежнему внутри SDK и не помечены
как deprecated. Депрекация назначена на <strong>ноябрьский релиз</strong>. Вот тогда часы и
пойдут.</p>
<p>Так что это хороший месяц для миграции и плохой месяц для паники. Окно
существует ровно затем, чтобы миграция прошла скучно.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="миграция--одна-команда-но-с-двумя-ловушками">Миграция — одна команда, но с двумя ловушками<a href="https://dartway.dev/ru/blog/flutter-3-47-what-breaks#%D0%BC%D0%B8%D0%B3%D1%80%D0%B0%D1%86%D0%B8%D1%8F--%D0%BE%D0%B4%D0%BD%D0%B0-%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4%D0%B0-%D0%BD%D0%BE-%D1%81-%D0%B4%D0%B2%D1%83%D0%BC%D1%8F-%D0%BB%D0%BE%D0%B2%D1%83%D1%88%D0%BA%D0%B0%D0%BC%D0%B8" class="hash-link" aria-label="Прямая ссылка на Миграция — одна команда, но с двумя ловушками" title="Прямая ссылка на Миграция — одна команда, но с двумя ловушками" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">dart fix --apply --code=migrate_design_widgets</span><br></div></code></pre></div></div>
<p><strong>Ловушка первая: простой <code>dart fix --apply</code> этого не сделает.</strong> Фикса нет в
наборе по умолчанию; без <code>--code=migrate_design_widgets</code> команда отработает,
отрапортует об успехе и не изменит ничего по делу. Это тот тип отказа, который
читается как «уже мигрировано».</p>
<p><strong>Ловушка вторая: она пишет <code>material_ui: any</code> в ваш <code>pubspec</code>.</strong> <code>any</code> значит,
что следующий ломающий релиз этого пакета приедет к вам в сборку молча. Замените
на каретку с версией сразу, как фикс отработает.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="утверждение-которое-неверно-changenotifier-никуда-не-уехал">Утверждение, которое неверно: <code>ChangeNotifier</code> никуда не уехал<a href="https://dartway.dev/ru/blog/flutter-3-47-what-breaks#%D1%83%D1%82%D0%B2%D0%B5%D1%80%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BA%D0%BE%D1%82%D0%BE%D1%80%D0%BE%D0%B5-%D0%BD%D0%B5%D0%B2%D0%B5%D1%80%D0%BD%D0%BE-changenotifier-%D0%BD%D0%B8%D0%BA%D1%83%D0%B4%D0%B0-%D0%BD%D0%B5-%D1%83%D0%B5%D1%85%D0%B0%D0%BB" class="hash-link" aria-label="Прямая ссылка на утверждение-которое-неверно-changenotifier-никуда-не-уехал" title="Прямая ссылка на утверждение-которое-неверно-changenotifier-никуда-не-уехал" translate="no">​</a></h2>
<p>Самая часто повторяемая фраза про этот релиз — что <code>ChangeNotifier</code> и компанию
вынесли из SDK в <code>package:listen</code>. В 3.47.2 произошло не это.</p>
<p>Классы по-прежнему в SDK и не помечены как deprecated. <code>listen</code> — это <strong>вторая,
независимая реализация тех же имён</strong>, а не новый дом для старых. Это разные
типы, и компилятор относится к ним именно так:</p>
<ul>
<li class="">передайте <code>ValueNotifier</code> из <code>listen</code> во flutter'овский
<code>ValueListenableBuilder</code> — и получите ошибку компиляции, а не тонкий баг в
рантайме;</li>
<li class="">то же самое в обратную сторону и для любой другой пары с общим именем.</li>
</ul>
<p>Где <code>listen</code> действительно к месту — код, в котором нет Flutter: сервер, CLI,
логика, общая для того и другого. Там типов из SDK никогда не было, а теперь есть
эквивалент. Внутри Flutter-приложения переход на него сегодня не даёт ничего и
стоит вам совместимости по типам с каждым виджетом, который ждёт версию из SDK.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="primary-constructors-сами-не-включатся">Primary constructors сами не включатся<a href="https://dartway.dev/ru/blog/flutter-3-47-what-breaks#primary-constructors-%D1%81%D0%B0%D0%BC%D0%B8-%D0%BD%D0%B5-%D0%B2%D0%BA%D0%BB%D1%8E%D1%87%D0%B0%D1%82%D1%81%D1%8F" class="hash-link" aria-label="Прямая ссылка на Primary constructors сами не включатся" title="Прямая ссылка на Primary constructors сами не включатся" translate="no">​</a></h2>
<p>В Dart 3.13 приехали primary constructors, и они за флагом версии языка. Без
<code>sdk: ^3.13.0</code> в <code>pubspec.yaml</code> вы получите <code>experiment_not_enabled</code> даже на
правильном SDK — тулчейн в порядке, это проект не просит.</p>
<p>Стоит, впрочем, сказать прямо: ценность экономии на бойлерплейте конструкторов
сильно падает, когда этот бойлерплейт пишет не человек. Если ваши конструкторы
генерируются, это изменение про читаемость, а не про продуктивность.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="остальное">Остальное<a href="https://dartway.dev/ru/blog/flutter-3-47-what-breaks#%D0%BE%D1%81%D1%82%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5" class="hash-link" aria-label="Прямая ссылка на Остальное" title="Прямая ссылка на Остальное" translate="no">​</a></h2>
<p>Impeller по умолчанию на десктопе, iOS 15 и macOS 12 как новые нижние границы и
работа над Wasm — всё реальное, ничего срочного, и ни на что из ваших дел в этом
месяце не влияет.</p>
<p><strong>Что реально сделать до ноября:</strong> прогнать миграцию в ветке, нормально
запинить <code>material_ui</code> и не трогать <code>ChangeNotifier</code>.</p>
<hr>
<p><em>Проверено миграцией живого проекта с 3.44.0 на 3.47.2 — <code>before/</code> и <code>after/</code>
собираются и проходят тесты каждый на своём SDK. Видеоразбор —
<a href="https://youtu.be/kNXMVEj_7bA" target="_blank" rel="noopener noreferrer" class="">на YouTube</a>.</em></p>]]></content>
        <author>
            <name>Evgenii Novikov</name>
            <uri>https://github.com/Disregard-Therest</uri>
        </author>
        <category label="Ecosystem" term="Ecosystem"/>
        <category label="Engineering" term="Engineering"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Один аккаунт, две двери: во что на самом деле обходится вход по телефону или почте]]></title>
        <id>https://dartway.dev/ru/blog/one-account-two-doors</id>
        <link href="https://dartway.dev/ru/blog/one-account-two-doors"/>
        <updated>2026-09-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Разрешить входить хоть по номеру телефона, хоть по почте выглядит правкой на одну строку. На деле это правка модели данных, и счёт приходит дублями аккаунтов, которые не слить.]]></summary>
        <content type="html"><![CDATA[
<p>Разрешить людям входить <strong>хоть</strong> по номеру телефона, <strong>хоть</strong> по почте выглядит
правкой на одну строку: расширяем поиск, принимаем оба варианта. Мы выкатили это
на прошлой неделе в живом проекте, и правка на одну строку оказалась правкой
модели данных — со счётом в придачу.</p>
<p>Вот во что она обходится на самом деле, в том порядке, в каком приходят счета.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="поиск-никогда-не-был-проблемой">Поиск никогда не был проблемой<a href="https://dartway.dev/ru/blog/one-account-two-doors#%D0%BF%D0%BE%D0%B8%D1%81%D0%BA-%D0%BD%D0%B8%D0%BA%D0%BE%D0%B3%D0%B4%D0%B0-%D0%BD%D0%B5-%D0%B1%D1%8B%D0%BB-%D0%BF%D1%80%D0%BE%D0%B1%D0%BB%D0%B5%D0%BC%D0%BE%D0%B9" class="hash-link" aria-label="Прямая ссылка на Поиск никогда не был проблемой" title="Прямая ссылка на Поиск никогда не был проблемой" translate="no">​</a></h2>
<p>Фреймворк хранил одну колонку <code>userIdentifier</code> и искал по строгому равенству.
Попроси его найти профиль по телефону <em>или</em> почте — и расширять просто негде: не
потому что запрос сложный, а потому что сама аутентификация требует, чтобы эта
колонка существовала. Убери её — и ядро падает на старте.</p>
<p>Так что первая правка — не «искать по двум полям». Это <strong>дать приложению право
голоса в том, как находится пользователь</strong>:</p>
<div class="language-dart codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-dart codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token class-name">DwAuthConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  findUserProfileByIdentifier</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">session</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> identifier</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">async</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)">// приложение само решает, что значит «тот же самый человек»</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div>
<p>Колонка перестаёт быть обязательной, идентификатор приходит уже нормализованным,
а фреймворк перестаёт делать вид, что знает, что такое личность в вашей предметной
области. Три обязанности переезжают в приложение, и документация говорит об этом
прямым текстом — резолвер, который молча делает не то, хуже, чем отсутствие
резолвера.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="поиск-по-двум-полям-сам-по-себе-ничего-не-чинит">Поиск по двум полям сам по себе ничего не чинит<a href="https://dartway.dev/ru/blog/one-account-two-doors#%D0%BF%D0%BE%D0%B8%D1%81%D0%BA-%D0%BF%D0%BE-%D0%B4%D0%B2%D1%83%D0%BC-%D0%BF%D0%BE%D0%BB%D1%8F%D0%BC-%D1%81%D0%B0%D0%BC-%D0%BF%D0%BE-%D1%81%D0%B5%D0%B1%D0%B5-%D0%BD%D0%B8%D1%87%D0%B5%D0%B3%D0%BE-%D0%BD%D0%B5-%D1%87%D0%B8%D0%BD%D0%B8%D1%82" class="hash-link" aria-label="Прямая ссылка на Поиск по двум полям сам по себе ничего не чинит" title="Прямая ссылка на Поиск по двум полям сам по себе ничего не чинит" translate="no">​</a></h2>
<p>Вот эта часть нас и удивила, и ради неё написан этот пост.</p>
<p>У пользователя, который зарегистрировался по телефону, <strong>колонка с почтой пустая</strong>.
Поиск по обоим полям не находит ничего нового, потому что во втором поле ничего
нет. Поиск одновременно корректен и бесполезен.</p>
<p>Чтобы заполнить эту колонку, нужен способ привязать второй канал к существующему
аккаунту — а значит, <code>addAuthProvider</code> и <code>changeIdentifier</code> должны реально
работать. У нас оба бросали <code>UnimplementedError</code>, и экрана для этого не было ни в
одном макете. Пока его нет, <strong>вход с другого канала по-прежнему тихо создаёт второй
аккаунт</strong>, ровно как и раньше.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="дубли-уже-есть">Дубли уже есть<a href="https://dartway.dev/ru/blog/one-account-two-doors#%D0%B4%D1%83%D0%B1%D0%BB%D0%B8-%D1%83%D0%B6%D0%B5-%D0%B5%D1%81%D1%82%D1%8C" class="hash-link" aria-label="Прямая ссылка на Дубли уже есть" title="Прямая ссылка на Дубли уже есть" translate="no">​</a></h2>
<p>Дальше идёт то, что отложить не получится. Мы пошли добавлять уникальные индексы на
<code>phone</code> и <code>email</code> и обнаружили, что <strong>уникальных индексов не было вообще</strong> — ни на
новых колонках, ни на старом <code>userIdentifier</code>. Две строки с одинаковым значением
уже были легальны, и поиск возвращал ту, которую базе захочется отдать.</p>
<p>Индекс не добавить, пока дубли не слиты. А дубли существуют именно <em>потому что</em>
вход со второго канала раньше создавал новый аккаунт. Баг и его разгребание — один
и тот же объект.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="слияние--это-событие-а-не-действие">Слияние — это событие, а не действие<a href="https://dartway.dev/ru/blog/one-account-two-doors#%D1%81%D0%BB%D0%B8%D1%8F%D0%BD%D0%B8%D0%B5--%D1%8D%D1%82%D0%BE-%D1%81%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D0%B5-%D0%B0-%D0%BD%D0%B5-%D0%B4%D0%B5%D0%B9%D1%81%D1%82%D0%B2%D0%B8%D0%B5" class="hash-link" aria-label="Прямая ссылка на Слияние — это событие, а не действие" title="Прямая ссылка на Слияние — это событие, а не действие" translate="no">​</a></h2>
<p>Слияние аккаунтов необратимо: один профиль поглощает другой, и после этого никто не
восстановит, что откуда пришло. Поэтому мы описали его как <strong>событие</strong> <code>ProfileMerge</code>,
а не как вызов метода — кто слил, кого и что переехало. Действие оставляет базу
консистентной, а историю потерянной; событие оставляет и то, и другое.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="что-мы-сказали-бы-себе-неделю-назад">Что мы сказали бы себе неделю назад<a href="https://dartway.dev/ru/blog/one-account-two-doors#%D1%87%D1%82%D0%BE-%D0%BC%D1%8B-%D1%81%D0%BA%D0%B0%D0%B7%D0%B0%D0%BB%D0%B8-%D0%B1%D1%8B-%D1%81%D0%B5%D0%B1%D0%B5-%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8E-%D0%BD%D0%B0%D0%B7%D0%B0%D0%B4" class="hash-link" aria-label="Прямая ссылка на Что мы сказали бы себе неделю назад" title="Прямая ссылка на Что мы сказали бы себе неделю назад" translate="no">​</a></h2>
<ul>
<li class="">«Вход по телефону или почте» — задача не про экран авторизации. Это задача про
<strong>идентичность профиля</strong>, а экран — последние пять процентов.</li>
<li class="">Выкатывайте <strong>привязку второго канала</strong> в том же релизе. Без неё фича — это поиск,
который ничего не находит.</li>
<li class="">Проверяйте уникальные индексы <strong>до</strong> того, как что-то пообещали. У нас их не было
за всю жизнь проекта, и никто не заметил, потому что ничто их не требовало.</li>
<li class="">Проверяйте на живой базе полным кругом: регистрация по телефону, привязка почты,
вход по почте в тот же аккаунт, одна строка в таблице. У нас 23 серверных теста,
включая этот проход; версия этой статьи, написанная по чтению кода, была бы неверна
в двух местах.</li>
</ul>
<p>Со стороны фреймворка это приехало в ядро <code>dartway</code> <code>0.12.1</code> — резолвер аутентификации
и работающий <code>changeIdentifier</code>. Если вы на более ранней версии, интересная часть —
миграция, а не API.</p>]]></content>
        <author>
            <name>Evgenii Novikov</name>
            <uri>https://github.com/Disregard-Therest</uri>
        </author>
        <category label="Engineering" term="Engineering"/>
        <category label="Case breakdown" term="Case breakdown"/>
    </entry>
</feed>