HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>В<a>первой части</a>нашей статьи мы привели несколько примеров и подвели промежуточные итоги. Пришло время поработать с поиском и сделать окончательные выводы.</p>
1 <p>В<a>первой части</a>нашей статьи мы привели несколько примеров и подвели промежуточные итоги. Пришло время поработать с поиском и сделать окончательные выводы.</p>
2 <p>Представим, что теперь нам необходимо найти в загруженных данных определенные элементы по вводимому в инпут-значению. Данный тест реализован следующим способом: имеется инпут, в который вводятся посимвольно 3 подстроки в 3 символа из числа подстрок, имеющихся в элементах списка. Количество элементов в массиве при поиске увеличено в 10 раз и составляет 22730 штук.</p>
2 <p>Представим, что теперь нам необходимо найти в загруженных данных определенные элементы по вводимому в инпут-значению. Данный тест реализован следующим способом: имеется инпут, в который вводятся посимвольно 3 подстроки в 3 символа из числа подстрок, имеющихся в элементах списка. Количество элементов в массиве при поиске увеличено в 10 раз и составляет 22730 штук.</p>
3 <p>Поиск осуществлялся в 2-х режимах -- примитивное наличие введенной строки в элементе из списка, а также с использованием алгоритма схожести строк.</p>
3 <p>Поиск осуществлялся в 2-х режимах -- примитивное наличие введенной строки в элементе из списка, а также с использованием алгоритма схожести строк.</p>
4 <p>Также, асинхронные варианты поиска -- compute/isolate не начинаются, пока не завершится предыдущий поиск. Т.е. схема такая -- введя первый символ в инпут, начинаем поиск, пока он не завершится -- данные не вернутся в основной поток и не перерисуется UI, второй символ в инпут не вводится. Когда все действия завершены, вводится второй символ и также наоборот. Это аналогично алгоритму, когда мы "копим" введенные пользователем символы, а затем отправляем всего один запрос, вместо отправки запроса на абсолютно каждый введенный символ, вне зависимости от того, с какой скоростью они вводились.</p>
4 <p>Также, асинхронные варианты поиска -- compute/isolate не начинаются, пока не завершится предыдущий поиск. Т.е. схема такая -- введя первый символ в инпут, начинаем поиск, пока он не завершится -- данные не вернутся в основной поток и не перерисуется UI, второй символ в инпут не вводится. Когда все действия завершены, вводится второй символ и также наоборот. Это аналогично алгоритму, когда мы "копим" введенные пользователем символы, а затем отправляем всего один запрос, вместо отправки запроса на абсолютно каждый введенный символ, вне зависимости от того, с какой скоростью они вводились.</p>
5 <p>Замеры времени отрисовки производились только во время ввода символов в поиск, т.е. операции подготовки данных и что-то другое, не влияли на собранные данные.</p>
5 <p>Замеры времени отрисовки производились только во время ввода символов в поиск, т.е. операции подготовки данных и что-то другое, не влияли на собранные данные.</p>
6 <p>Для начала, вспомогательные функции, функция поиска и другой общий код:</p>
6 <p>Для начала, вспомогательные функции, функция поиска и другой общий код:</p>
7 /// Функция для создания копии элементов /// используемых как исходные при фильтрации void cacheItems() { _notFilteredItems.clear(); final List&lt;Item&gt; multipliedItems = []; for (int i = 0; i &lt; 10; i++) { multipliedItems.addAll(items); } _notFilteredItems.addAll(multipliedItems); } /// Функция, запускающая тестовый сценарий /// по вводу символов в текстовый инпут Future&lt;void&gt; _testSearch() async { List&lt;String&gt; words = items.map((Item item) =&gt; item.profile.replaceAll('https://opencollective.com/', '')).toSet().toList(); words = words .map((String word) { final String newWord = word.substring(0, min(word.length, 3)); return newWord; }) .toSet() .take(3) .toList(); /// Стартуем счетчик кадров _startFpsMeter(); for (String word in words) { final List&lt;String&gt; letters = word.split(''); String search = ''; for (String letter in letters) { search += letter; await _setWord(search); } while (search.isNotEmpty) { search = search.substring(0, search.length - 1); await _setWord(search); } } /// Останавливаем счетчик _stopFpsMeter(); } /// Вводим символы с задержкой /// в 800мс, но если данные из асинхронного /// фильтра (computed / isolate) еще не пришли, /// то ждем их Future&lt;void&gt; _setWord(String word) async { if (!canPlaceNextLetter) { await wait(800); await _setWord(word); } else { searchController.value = TextEditingValue(text: word); await wait(800); } } /// В зависимости от установленного флага [USE_SIMILARITY] /// используется или нет поиск со схожестью строк List&lt;Item&gt; filterItems(Packet2&lt;List&lt;Item&gt;, String&gt; itemsAndInputValue) { return itemsAndInputValue.value.where((Item item) { return item.profile.contains(itemsAndInputValue.value2) || (USE_SIMILARITY &amp;&amp; isStringsSimilar(item.profile, itemsAndInputValue.value2)); }).toList(); } bool isStringsSimilar(String first, String second) { return max(StringSimilarity.compareTwoStrings(first, second), StringSimilarity.compareTwoStrings(second, first)) &gt;= 0.3); }<h2>Поиск в главном потоке</h2>
7 /// Функция для создания копии элементов /// используемых как исходные при фильтрации void cacheItems() { _notFilteredItems.clear(); final List&lt;Item&gt; multipliedItems = []; for (int i = 0; i &lt; 10; i++) { multipliedItems.addAll(items); } _notFilteredItems.addAll(multipliedItems); } /// Функция, запускающая тестовый сценарий /// по вводу символов в текстовый инпут Future&lt;void&gt; _testSearch() async { List&lt;String&gt; words = items.map((Item item) =&gt; item.profile.replaceAll('https://opencollective.com/', '')).toSet().toList(); words = words .map((String word) { final String newWord = word.substring(0, min(word.length, 3)); return newWord; }) .toSet() .take(3) .toList(); /// Стартуем счетчик кадров _startFpsMeter(); for (String word in words) { final List&lt;String&gt; letters = word.split(''); String search = ''; for (String letter in letters) { search += letter; await _setWord(search); } while (search.isNotEmpty) { search = search.substring(0, search.length - 1); await _setWord(search); } } /// Останавливаем счетчик _stopFpsMeter(); } /// Вводим символы с задержкой /// в 800мс, но если данные из асинхронного /// фильтра (computed / isolate) еще не пришли, /// то ждем их Future&lt;void&gt; _setWord(String word) async { if (!canPlaceNextLetter) { await wait(800); await _setWord(word); } else { searchController.value = TextEditingValue(text: word); await wait(800); } } /// В зависимости от установленного флага [USE_SIMILARITY] /// используется или нет поиск со схожестью строк List&lt;Item&gt; filterItems(Packet2&lt;List&lt;Item&gt;, String&gt; itemsAndInputValue) { return itemsAndInputValue.value.where((Item item) { return item.profile.contains(itemsAndInputValue.value2) || (USE_SIMILARITY &amp;&amp; isStringsSimilar(item.profile, itemsAndInputValue.value2)); }).toList(); } bool isStringsSimilar(String first, String second) { return max(StringSimilarity.compareTwoStrings(first, second), StringSimilarity.compareTwoStrings(second, first)) &gt;= 0.3); }<h2>Поиск в главном потоке</h2>
8 Future&lt;void&gt; runSearchOnMainThread() async { cacheItems(); isLoading = true; notifyListeners(); searchController.addListener(_searchOnMainThread); await _testSearch(); searchController.removeListener(_searchOnMainThread); isLoading = false; notifyListeners(); } void _searchOnMainThread() { final String searchValue = searchController.text; if (searchValue.isEmpty &amp;&amp; items.length != _notFilteredItems.length) { items.clear(); items.addAll(_notFilteredItems); notifyListeners(); return; } items.clear(); /// Packet2 - обертка для двух значений items.addAll(filterItems(Packet2(_notFilteredItems, searchValue))); notifyListeners(); }<p>Простой поиск:</p>
8 Future&lt;void&gt; runSearchOnMainThread() async { cacheItems(); isLoading = true; notifyListeners(); searchController.addListener(_searchOnMainThread); await _testSearch(); searchController.removeListener(_searchOnMainThread); isLoading = false; notifyListeners(); } void _searchOnMainThread() { final String searchValue = searchController.text; if (searchValue.isEmpty &amp;&amp; items.length != _notFilteredItems.length) { items.clear(); items.addAll(_notFilteredItems); notifyListeners(); return; } items.clear(); /// Packet2 - обертка для двух значений items.addAll(filterItems(Packet2(_notFilteredItems, searchValue))); notifyListeners(); }<p>Простой поиск:</p>
9 <ul><li>Среднее время кадра -- 21.588ms / 46.32FPS</li>
9 <ul><li>Среднее время кадра -- 21.588ms / 46.32FPS</li>
10 <li>Медианное время кадра -- 11.154ms / 89.65FPS</li>
10 <li>Медианное время кадра -- 11.154ms / 89.65FPS</li>
11 <li>Максимальное время кадра -- 668,986ms / 1.50FPS</li>
11 <li>Максимальное время кадра -- 668,986ms / 1.50FPS</li>
12 </ul><p>Поиск со схожестью:</p>
12 </ul><p>Поиск со схожестью:</p>
13 <ul><li>Среднее время кадра -- 43,123ms / 23.19FPS</li>
13 <ul><li>Среднее время кадра -- 43,123ms / 23.19FPS</li>
14 <li>Медианное время кадра -- 11,152ms / 89.67FPS</li>
14 <li>Медианное время кадра -- 11,152ms / 89.67FPS</li>
15 <li>Максимальное время кадра -- 2 440,910ms / 0.41FPS</li>
15 <li>Максимальное время кадра -- 2 440,910ms / 0.41FPS</li>
16 </ul><h2>Поиск через Compute</h2>
16 </ul><h2>Поиск через Compute</h2>
17 Future&lt;void&gt; runSearchWithCompute() async { cacheItems(); isLoading = true; notifyListeners(); searchController.addListener(_searchWithCompute); await _testSearch(); searchController.removeListener(_searchWithCompute); isLoading = false; notifyListeners(); } Future&lt;void&gt; _searchWithCompute() async { canPlaceNextLetter = false; /// Перед началом фильтрации /// устанавливаем флаг, который будет сигнализировать /// о том, что происходит асинхронная фильтрация isSearching = true; notifyListeners(); final String searchValue = searchController.text; if (searchValue.isEmpty &amp;&amp; items.length != _notFilteredItems.length) { items.clear(); items.addAll(_notFilteredItems); isSearching = false; notifyListeners(); await wait(800); canPlaceNextLetter = true; return; } final List&lt;Item&gt; filteredItems = await compute(filterItems, Packet2(_notFilteredItems, searchValue)); /// После окончания фильтрации убираем сигнал isSearching = false; notifyListeners(); await wait(800); items.clear(); items.addAll(filteredItems); notifyListeners(); canPlaceNextLetter = true; }<p>Простой поиск:</p>
17 Future&lt;void&gt; runSearchWithCompute() async { cacheItems(); isLoading = true; notifyListeners(); searchController.addListener(_searchWithCompute); await _testSearch(); searchController.removeListener(_searchWithCompute); isLoading = false; notifyListeners(); } Future&lt;void&gt; _searchWithCompute() async { canPlaceNextLetter = false; /// Перед началом фильтрации /// устанавливаем флаг, который будет сигнализировать /// о том, что происходит асинхронная фильтрация isSearching = true; notifyListeners(); final String searchValue = searchController.text; if (searchValue.isEmpty &amp;&amp; items.length != _notFilteredItems.length) { items.clear(); items.addAll(_notFilteredItems); isSearching = false; notifyListeners(); await wait(800); canPlaceNextLetter = true; return; } final List&lt;Item&gt; filteredItems = await compute(filterItems, Packet2(_notFilteredItems, searchValue)); /// После окончания фильтрации убираем сигнал isSearching = false; notifyListeners(); await wait(800); items.clear(); items.addAll(filteredItems); notifyListeners(); canPlaceNextLetter = true; }<p>Простой поиск:</p>
18 <ul><li>Среднее время кадра -- 12,682ms / 78.85FPS</li>
18 <ul><li>Среднее время кадра -- 12,682ms / 78.85FPS</li>
19 <li>Медианное время кадра -- 11,154ms / 89.65FPS</li>
19 <li>Медианное время кадра -- 11,154ms / 89.65FPS</li>
20 <li>Максимальное время кадра -- 111,544ms / 8.97FPS</li>
20 <li>Максимальное время кадра -- 111,544ms / 8.97FPS</li>
21 </ul><p>Поиск со схожестью:</p>
21 </ul><p>Поиск со схожестью:</p>
22 <ul><li>Среднее время кадра -- 12,515ms / 79.90FPS</li>
22 <ul><li>Среднее время кадра -- 12,515ms / 79.90FPS</li>
23 <li>Медианное время кадра -- 11,153ms / 89.66FPS</li>
23 <li>Медианное время кадра -- 11,153ms / 89.66FPS</li>
24 <li>Максимальное время кадра -- 111,527ms / 8.97FPS</li>
24 <li>Максимальное время кадра -- 111,527ms / 8.97FPS</li>
25 </ul><h2>Поиск с помощью Isolate</h2>
25 </ul><h2>Поиск с помощью Isolate</h2>
26 <p>Немного кода:</p>
26 <p>Немного кода:</p>
27 /// Запускаем операцию в изоляте Future&lt;void&gt; runSearchInIsolate() async { send(Events.cacheItems); } void _middleLoadingEvent() { final double time = bench.endTimer('Load items in separate isolate'); requestDurations.add(time); bench.startTimer('Load items in separate isolate'); } /// Этот метод запускается на событие [Events.cacheItems], /// отправленное из изолята Future&lt;void&gt; _startSearchOnIsolate() async { isLoading = true; notifyListeners(); searchController.addListener(_searchInIsolate); await _testSearch(); searchController.removeListener(_searchInIsolate); isLoading = false; notifyListeners(); } /// На каждое изменение инпута отсылается сообщение в изолят void _searchInIsolate() { canPlaceNextLetter = false; isSearching = true; notifyListeners(); send(Events.startSearch, searchController.text); } /// Запись в реактивный стейт данных из изолята Future&lt;void&gt; _setFilteredItems(List&lt;Item&gt; filteredItems) async { isSearching = false; notifyListeners(); await wait(800); items.clear(); items.addAll(filteredItems); notifyListeners(); canPlaceNextLetter = true; } Future&lt;void&gt; _endLoadingEvents(List&lt;Item&gt; items) async { this.items.clear(); this.items.addAll(items); final double time = bench.endTimer('Load items in separate isolate'); requestDurations.add(time); await wait(800); isLoading = false; notifyListeners(); _stopFpsMeter(); print('Load items in isolate -&gt;' + requestDurations.join(' ').replaceAll('.', ',')); requestDurations.clear(); }<p>А это методы, находящиеся в бэкенде, который работает в стороннем изоляте:</p>
27 /// Запускаем операцию в изоляте Future&lt;void&gt; runSearchInIsolate() async { send(Events.cacheItems); } void _middleLoadingEvent() { final double time = bench.endTimer('Load items in separate isolate'); requestDurations.add(time); bench.startTimer('Load items in separate isolate'); } /// Этот метод запускается на событие [Events.cacheItems], /// отправленное из изолята Future&lt;void&gt; _startSearchOnIsolate() async { isLoading = true; notifyListeners(); searchController.addListener(_searchInIsolate); await _testSearch(); searchController.removeListener(_searchInIsolate); isLoading = false; notifyListeners(); } /// На каждое изменение инпута отсылается сообщение в изолят void _searchInIsolate() { canPlaceNextLetter = false; isSearching = true; notifyListeners(); send(Events.startSearch, searchController.text); } /// Запись в реактивный стейт данных из изолята Future&lt;void&gt; _setFilteredItems(List&lt;Item&gt; filteredItems) async { isSearching = false; notifyListeners(); await wait(800); items.clear(); items.addAll(filteredItems); notifyListeners(); canPlaceNextLetter = true; } Future&lt;void&gt; _endLoadingEvents(List&lt;Item&gt; items) async { this.items.clear(); this.items.addAll(items); final double time = bench.endTimer('Load items in separate isolate'); requestDurations.add(time); await wait(800); isLoading = false; notifyListeners(); _stopFpsMeter(); print('Load items in isolate -&gt;' + requestDurations.join(' ').replaceAll('.', ',')); requestDurations.clear(); }<p>А это методы, находящиеся в бэкенде, который работает в стороннем изоляте:</p>
28 /// Обработчик события [Events.cacheItems] void _cacheItems() { _notFilteredItems.clear(); final List&lt;Item&gt; multipliedItems = []; for (int i = 0; i &lt; 10; i++) { multipliedItems.addAll(_items); } _notFilteredItems.addAll(multipliedItems); send(Events.cacheItems); } /// На каждое событие [Events.startSearch] вызывается данный метод /// фильтрующий элементы и отсылающий отфильтрованное в легкий стейт void _filterItems(String searchValue) { if (searchValue.isEmpty) { _items.clear(); _items.addAll(_notFilteredItems); send(ThirdEvents.setFilteredItems, _items); return; } final List&lt;Item&gt; filteredItems = filterItems(Packet2(_notFilteredItems, searchValue)); _items.clear(); _items.addAll(filteredItems); send(Events.setFilteredItems, _items); }<p>Простой поиск:</p>
28 /// Обработчик события [Events.cacheItems] void _cacheItems() { _notFilteredItems.clear(); final List&lt;Item&gt; multipliedItems = []; for (int i = 0; i &lt; 10; i++) { multipliedItems.addAll(_items); } _notFilteredItems.addAll(multipliedItems); send(Events.cacheItems); } /// На каждое событие [Events.startSearch] вызывается данный метод /// фильтрующий элементы и отсылающий отфильтрованное в легкий стейт void _filterItems(String searchValue) { if (searchValue.isEmpty) { _items.clear(); _items.addAll(_notFilteredItems); send(ThirdEvents.setFilteredItems, _items); return; } final List&lt;Item&gt; filteredItems = filterItems(Packet2(_notFilteredItems, searchValue)); _items.clear(); _items.addAll(filteredItems); send(Events.setFilteredItems, _items); }<p>Простой поиск:</p>
29 <ul><li>Среднее время кадра -- 11,354ms / 88.08FPS</li>
29 <ul><li>Среднее время кадра -- 11,354ms / 88.08FPS</li>
30 <li>Медианное время кадра -- 11,153ms / 89.66FPS</li>
30 <li>Медианное время кадра -- 11,153ms / 89.66FPS</li>
31 <li>Максимальное время кадра -- 33,455ms / 29.89FPS</li>
31 <li>Максимальное время кадра -- 33,455ms / 29.89FPS</li>
32 </ul><p>Поиск со схожестью:</p>
32 </ul><p>Поиск со схожестью:</p>
33 <ul><li>Среднее время кадра -- 11,353ms / 88.08FPS</li>
33 <ul><li>Среднее время кадра -- 11,353ms / 88.08FPS</li>
34 <li>Медианное время кадра -- 11,153ms / 89.66FPS</li>
34 <li>Медианное время кадра -- 11,153ms / 89.66FPS</li>
35 <li>Максимальное время кадра -- 33,459ms / 29.89FPS</li>
35 <li>Максимальное время кадра -- 33,459ms / 29.89FPS</li>
36 </ul><h2>Еще одни выводы</h2>
36 </ul><h2>Еще одни выводы</h2>
37 <p>Из этой таблички и предыдущего исследования следует, что:</p>
37 <p>Из этой таблички и предыдущего исследования следует, что:</p>
38 <ul><li>Главный поток не следует использовать для операций &gt; 16ms (чтобы обеспечить, хотя бы, 60FPS)</li>
38 <ul><li>Главный поток не следует использовать для операций &gt; 16ms (чтобы обеспечить, хотя бы, 60FPS)</li>
39 <li>Compute технически подходит для частых и тяжелых операций, но накладывает overhead в те же 150ms, а также имеет более нестабильную производительность, по сравнению с постоянным изолятом (вероятно, это связано с тем, что каждый раз открывается, и, после завершения операции -- закрывается изолят, что также требует ресурсов)</li>
39 <li>Compute технически подходит для частых и тяжелых операций, но накладывает overhead в те же 150ms, а также имеет более нестабильную производительность, по сравнению с постоянным изолятом (вероятно, это связано с тем, что каждый раз открывается, и, после завершения операции -- закрывается изолят, что также требует ресурсов)</li>
40 <li>Isolate -- самый сложный в написании кода способ достижения максимальной производительности приложения на Flutter</li>
40 <li>Isolate -- самый сложный в написании кода способ достижения максимальной производительности приложения на Flutter</li>
41 </ul><p>Что же, кажется, что изоляты -- это идеальный способ достижения результата, и даже Google советует использовать именно их для всех тяжелых операций (это для красного словца, пруфов я не нашел?). Но нужно писать много кода. На самом деле, все что написано выше -- это результат, достигнутый с использованием представленной в самом начале библиотеки, без нее -- придется написать намного, намнооого больше. К тому же, данный алгоритм поиска можно оптимизировать -- после фильтрации всех элементов отправлять фронту только маленькую порцию данных -- это отнимет меньше ресурсов, а уже после ее передачи отправлять все остальное.</p>
41 </ul><p>Что же, кажется, что изоляты -- это идеальный способ достижения результата, и даже Google советует использовать именно их для всех тяжелых операций (это для красного словца, пруфов я не нашел?). Но нужно писать много кода. На самом деле, все что написано выше -- это результат, достигнутый с использованием представленной в самом начале библиотеки, без нее -- придется написать намного, намнооого больше. К тому же, данный алгоритм поиска можно оптимизировать -- после фильтрации всех элементов отправлять фронту только маленькую порцию данных -- это отнимет меньше ресурсов, а уже после ее передачи отправлять все остальное.</p>
42 <p>Также я проводил эксперименты по пропускной способности канала связи между изолятами. Для ее оценки использовалась таких сущностей:</p>
42 <p>Также я проводил эксперименты по пропускной способности канала связи между изолятами. Для ее оценки использовалась таких сущностей:</p>
43 class Item { const Item( this.id, this.createdAt, this.profile, this.imageUrl, ); final int id; final DateTime createdAt; final String profile; final String imageUrl; }<p>И получилось следующее -- при одновременной передаче 5000 элементов, время, которое уходит на копирование данных, не влияет на UI, т.е. частота отрисовки не уменьшается. Было передано 1 000 000 таких элементов пачками по 5 000 штук за раз с принудительной паузой между передачей пачек в 8ms, через Future&lt;void&gt;.delayed , при этом частота кадров не опускалась ниже 80FPS. К сожалению, делал я этот эксперимент задолго до написания данной статьи и сухих цифр нет (если будет запрос -- то появятся).</p>
43 class Item { const Item( this.id, this.createdAt, this.profile, this.imageUrl, ); final int id; final DateTime createdAt; final String profile; final String imageUrl; }<p>И получилось следующее -- при одновременной передаче 5000 элементов, время, которое уходит на копирование данных, не влияет на UI, т.е. частота отрисовки не уменьшается. Было передано 1 000 000 таких элементов пачками по 5 000 штук за раз с принудительной паузой между передачей пачек в 8ms, через Future&lt;void&gt;.delayed , при этом частота кадров не опускалась ниже 80FPS. К сожалению, делал я этот эксперимент задолго до написания данной статьи и сухих цифр нет (если будет запрос -- то появятся).</p>
44 <p>Многим может показаться сложным или не нужным разбираться с изолятами, и люди останавливаются на compute. Тут на помощь может прийти еще одна функциональность данного пакета, которая приравнивает API к простоте compute, а возможностей в итоге дает намного больше.</p>
44 <p>Многим может показаться сложным или не нужным разбираться с изолятами, и люди останавливаются на compute. Тут на помощь может прийти еще одна функциональность данного пакета, которая приравнивает API к простоте compute, а возможностей в итоге дает намного больше.</p>
45 <p>Вот пример:</p>
45 <p>Вот пример:</p>
46 /// Frontend part Future&lt;void&gt; decrement([int diff = 1]) async { counter = await runBackendMethod&lt;int, int&gt;(Events.decrement, diff); } /// ----- /// Backend part Future&lt;int&gt; _decrement(int diff) async { counter -= diff; return counter; }<p>Благодаря данному подходу можно просто вызвать функцию бэкенда по ID, которому эта функция соответствуют. Соответствие ID -- метод задается в предопределенных геттерах:</p>
46 /// Frontend part Future&lt;void&gt; decrement([int diff = 1]) async { counter = await runBackendMethod&lt;int, int&gt;(Events.decrement, diff); } /// ----- /// Backend part Future&lt;int&gt; _decrement(int diff) async { counter -= diff; return counter; }<p>Благодаря данному подходу можно просто вызвать функцию бэкенда по ID, которому эта функция соответствуют. Соответствие ID -- метод задается в предопределенных геттерах:</p>
47 /// Frontend part /// Данный блок отвечает за обработку событий из изолята @override Map&lt;Events, Function&gt; get tasks =&gt; { Events.increment: _setCounter, Events.decrement: _setCounter, Events.error: _setCounter, }; /// ----- /// Backend part /// А данный -- за обработку событий из главного потока @override Map&lt;Events, Function&gt; get operations =&gt; { Events.increment: _increment, Events.decrement: _decrement, };<p>Таким образом мы получаем два способа взаимодействия:</p>
47 /// Frontend part /// Данный блок отвечает за обработку событий из изолята @override Map&lt;Events, Function&gt; get tasks =&gt; { Events.increment: _setCounter, Events.decrement: _setCounter, Events.error: _setCounter, }; /// ----- /// Backend part /// А данный -- за обработку событий из главного потока @override Map&lt;Events, Function&gt; get operations =&gt; { Events.increment: _increment, Events.decrement: _decrement, };<p>Таким образом мы получаем два способа взаимодействия:</p>
48 <p><strong>1 Асинхронное общение через явную передачу сообщений</strong></p>
48 <p><strong>1 Асинхронное общение через явную передачу сообщений</strong></p>
49 <p>1.1 Frontend-стейт (тот, что крутится в главном потоке, замиксованный с BackendMixin&lt;EventType&gt; ) отправляет событие в Backend-стейт используя метод send, передавая в сообщении ID события и необязательный аргумент.</p>
49 <p>1.1 Frontend-стейт (тот, что крутится в главном потоке, замиксованный с BackendMixin&lt;EventType&gt; ) отправляет событие в Backend-стейт используя метод send, передавая в сообщении ID события и необязательный аргумент.</p>
50 enum Events { increment, } class FirstState with BackendMixin&lt;Events&gt; { int counter = 0; void increment([int diff = 1]) { send(Events.increment, diff); } void _setCounter(int value) { counter = value; notifyListeners(); } @override Map&lt;Events, Function&gt; get tasks =&gt; { Events.increment: _setCounter, }; }<p>1.2 Это сообщение передается в бэкенд и обрабатывается там</p>
50 enum Events { increment, } class FirstState with BackendMixin&lt;Events&gt; { int counter = 0; void increment([int diff = 1]) { send(Events.increment, diff); } void _setCounter(int value) { counter = value; notifyListeners(); } @override Map&lt;Events, Function&gt; get tasks =&gt; { Events.increment: _setCounter, }; }<p>1.2 Это сообщение передается в бэкенд и обрабатывается там</p>
51 class FirstBackend extends Backend&lt;Events&gt; { FirstBackend(SendPort toFrontend) : super(toFrontend); int counter = 0; void _increment(int diff) { counter += diff; send(Events.increment, counter); } @override Map&lt;Events, Function&gt; get operations =&gt; { Events.increment: _increment, }; }<p>1.3 Backend-стейт возвращает результат в реактивный стейт главного потока и готово! Есть два способа вернуть результат -- возврат ответа методом бэкенда (return) (тогда ответ будет отправлен с тем же ID сообщения, что и был получен), а второй -- явно вызвать метод send. При этом можно отправлять в реактивный стейт какие угодно сообщения с любыми, заданными вами ID. Главное -- чтобы этим ID были заданы методы-обработчики.</p>
51 class FirstBackend extends Backend&lt;Events&gt; { FirstBackend(SendPort toFrontend) : super(toFrontend); int counter = 0; void _increment(int diff) { counter += diff; send(Events.increment, counter); } @override Map&lt;Events, Function&gt; get operations =&gt; { Events.increment: _increment, }; }<p>1.3 Backend-стейт возвращает результат в реактивный стейт главного потока и готово! Есть два способа вернуть результат -- возврат ответа методом бэкенда (return) (тогда ответ будет отправлен с тем же ID сообщения, что и был получен), а второй -- явно вызвать метод send. При этом можно отправлять в реактивный стейт какие угодно сообщения с любыми, заданными вами ID. Главное -- чтобы этим ID были заданы методы-обработчики.</p>
52 <p>Схематично, первый способ выглядит так:</p>
52 <p>Схематично, первый способ выглядит так:</p>
53 <p>Желтая двусторонняя стрелка -- взаимодействие с какими-либо сервисами извне, например -- неким сервером. А фиолетовая, идущая от сервера к бэку -- это входящие сообщения от того же сервера, например -- WebSocket.</p>
53 <p>Желтая двусторонняя стрелка -- взаимодействие с какими-либо сервисами извне, например -- неким сервером. А фиолетовая, идущая от сервера к бэку -- это входящие сообщения от того же сервера, например -- WebSocket.</p>
54 <p><strong>2 Синхронное общение через вызов функции бэкенда по ее ID</strong></p>
54 <p><strong>2 Синхронное общение через вызов функции бэкенда по ее ID</strong></p>
55 <p>2.1 Frontend использует метод runBackendMethod , указывая ID, чтобы вызвать метод бэка, ему соответствующий, получая ответ тут же. В таком способе не обязательно даже указывать что-либо в списке задач (tasks) вашего фронта. При этом, как показано в коде ниже, вы можете переопределить метод onBackendResponse в вашем фронте, который вызывается после каждого получения вашим фронт-стейтом сообщений от бэка.</p>
55 <p>2.1 Frontend использует метод runBackendMethod , указывая ID, чтобы вызвать метод бэка, ему соответствующий, получая ответ тут же. В таком способе не обязательно даже указывать что-либо в списке задач (tasks) вашего фронта. При этом, как показано в коде ниже, вы можете переопределить метод onBackendResponse в вашем фронте, который вызывается после каждого получения вашим фронт-стейтом сообщений от бэка.</p>
56 enum Events { decrement, } class FirstState with ChangeNotifier, BackendMixin&lt;Events&gt; { int counter = 0; Future&lt;void&gt; decrement([int diff = 1]) async { counter = await runBackendMethod&lt;int, int&gt;(Events.decrement, diff); } /// Automatically notification after any event from backend @override void onBackendResponse() { notifyListeners(); } }<p>2.2 Backend-метод обрабатывает пришедшее событие, и просто возвращает результат. В данном случае есть одно ограничение -- методы бэка, вызываемые "синхронно", не должны вызывать метод send, с тем же ID, которому они соответствуют. В данном примере метод _decrement не должен вызывать метод send(Events.decrement). При этом любые другие сообщения он отправлять может.</p>
56 enum Events { decrement, } class FirstState with ChangeNotifier, BackendMixin&lt;Events&gt; { int counter = 0; Future&lt;void&gt; decrement([int diff = 1]) async { counter = await runBackendMethod&lt;int, int&gt;(Events.decrement, diff); } /// Automatically notification after any event from backend @override void onBackendResponse() { notifyListeners(); } }<p>2.2 Backend-метод обрабатывает пришедшее событие, и просто возвращает результат. В данном случае есть одно ограничение -- методы бэка, вызываемые "синхронно", не должны вызывать метод send, с тем же ID, которому они соответствуют. В данном примере метод _decrement не должен вызывать метод send(Events.decrement). При этом любые другие сообщения он отправлять может.</p>
57 class FirstBackend extends Backend&lt;Events&gt; { FirstBackend(SendPort toFrontend) : super(toFrontend); int counter = 0; /// Or, you can simply return a value Future&lt;int&gt; _decrement(int diff) async { counter -= diff; return counter; } @override Map&lt;Events, Function&gt; get operations =&gt; { Events.decrement: _decrement, }; }<p>Схема второго способа похожа на первый, за тем исключением, что во фронте вам не нужно писать обработчики событий, прилетающих с бэка.</p>
57 class FirstBackend extends Backend&lt;Events&gt; { FirstBackend(SendPort toFrontend) : super(toFrontend); int counter = 0; /// Or, you can simply return a value Future&lt;int&gt; _decrement(int diff) async { counter -= diff; return counter; } @override Map&lt;Events, Function&gt; get operations =&gt; { Events.decrement: _decrement, }; }<p>Схема второго способа похожа на первый, за тем исключением, что во фронте вам не нужно писать обработчики событий, прилетающих с бэка.</p>
58 <h2>Что бы еще добавить...</h2>
58 <h2>Что бы еще добавить...</h2>
59 <p>Чтобы использовать такую связку -- необходимо эти бэкенды создавать. Для этого в BackendMixin&lt;EventType&gt; заложен механизм создания бэка -- метод initBackend. В данный метод необходимо передать функцию-фабрику по созданию бэкенда. Это должна быть чистая функция высшего уровня (top-level, как гласит документация Flutter), либо статический метод класса. Время создания одного изолята ~200ms.</p>
59 <p>Чтобы использовать такую связку -- необходимо эти бэкенды создавать. Для этого в BackendMixin&lt;EventType&gt; заложен механизм создания бэка -- метод initBackend. В данный метод необходимо передать функцию-фабрику по созданию бэкенда. Это должна быть чистая функция высшего уровня (top-level, как гласит документация Flutter), либо статический метод класса. Время создания одного изолята ~200ms.</p>
60 enum Events { increment, decrement, } class FirstState with ChangeNotifier, BackendMixin&lt;Events&gt; { int counter = 0; void increment([int diff = 1]) { send(Events.increment, diff); } Future&lt;void&gt; decrement([int diff = 1]) async { counter = await runBackendMethod&lt;int, int&gt;(Events.decrement, diff); } void _setCounter(int value) { counter = value; } Future&lt;void&gt; initState() async { await initBackend(createFirstBackend); } /// Automatically notification after any event from backend @override void onBackendResponse() { notifyListeners(); } @override Map&lt;Events, Function&gt; get tasks =&gt; { Events.increment: _setCounter, }; }<p>Пример функции-создателя Backend-части:</p>
60 enum Events { increment, decrement, } class FirstState with ChangeNotifier, BackendMixin&lt;Events&gt; { int counter = 0; void increment([int diff = 1]) { send(Events.increment, diff); } Future&lt;void&gt; decrement([int diff = 1]) async { counter = await runBackendMethod&lt;int, int&gt;(Events.decrement, diff); } void _setCounter(int value) { counter = value; } Future&lt;void&gt; initState() async { await initBackend(createFirstBackend); } /// Automatically notification after any event from backend @override void onBackendResponse() { notifyListeners(); } @override Map&lt;Events, Function&gt; get tasks =&gt; { Events.increment: _setCounter, }; }<p>Пример функции-создателя Backend-части:</p>
61 typedef Creator&lt;TDataType&gt; = void Function(BackendArgument&lt;TDataType&gt; argument); void createFirstBackend(BackendArgument&lt;void&gt; argument) { FirstBackend(argument.toFrontend); } @protected Future&lt;void&gt; initBackend&lt;TDataType extends Object&gt;(Creator&lt;TDataType&gt; creator, {TDataType data, ErrorHandler errorHandler}) async { /// ... }<h2>Ограничения</h2>
61 typedef Creator&lt;TDataType&gt; = void Function(BackendArgument&lt;TDataType&gt; argument); void createFirstBackend(BackendArgument&lt;void&gt; argument) { FirstBackend(argument.toFrontend); } @protected Future&lt;void&gt; initBackend&lt;TDataType extends Object&gt;(Creator&lt;TDataType&gt; creator, {TDataType data, ErrorHandler errorHandler}) async { /// ... }<h2>Ограничения</h2>
62 <ul><li>Все тоже самое, что есть у обычного изолята</li>
62 <ul><li>Все тоже самое, что есть у обычного изолята</li>
63 <li>Для каждого создающегося "бэкенда" в данный момент создается свой изолят и при слишком большом количестве бэкендов -- время их создания становится ощутимым, особенно, если инициализировать все их, скажем, при загрузке приложения. Я проводил эксперименты, запуская одновременно 30 бэкендов -- время загрузки на указанном выше телефоне в --release режиме заняло 6 с небольшим секунд.</li>
63 <li>Для каждого создающегося "бэкенда" в данный момент создается свой изолят и при слишком большом количестве бэкендов -- время их создания становится ощутимым, особенно, если инициализировать все их, скажем, при загрузке приложения. Я проводил эксперименты, запуская одновременно 30 бэкендов -- время загрузки на указанном выше телефоне в --release режиме заняло 6 с небольшим секунд.</li>
64 <li>Есть некоторые сложности с обработкой ошибок, возникающих в изолятах (бэкендах). Тут, если вас заинтересует данный пакет, следует подробнее ознакомиться с методом initBackend из BackendMixin.</li>
64 <li>Есть некоторые сложности с обработкой ошибок, возникающих в изолятах (бэкендах). Тут, если вас заинтересует данный пакет, следует подробнее ознакомиться с методом initBackend из BackendMixin.</li>
65 <li>Сложность написания кода выше, по сравнению с хранением логики только в главном потоке</li>
65 <li>Сложность написания кода выше, по сравнению с хранением логики только в главном потоке</li>
66 </ul><h2>Чек-лист для использования</h2>
66 </ul><h2>Чек-лист для использования</h2>
67 <p>Тут все просто, вам не нужно использовать изоляты (как отдельно, так и с помощью данного пакета), если:</p>
67 <p>Тут все просто, вам не нужно использовать изоляты (как отдельно, так и с помощью данного пакета), если:</p>
68 <ul><li>Производительность вашего приложения не падает при различных операциях</li>
68 <ul><li>Производительность вашего приложения не падает при различных операциях</li>
69 <li>Для узких мест вам достаточно compute</li>
69 <li>Для узких мест вам достаточно compute</li>
70 <li>Вам не хочется разбираться с изолятами</li>
70 <li>Вам не хочется разбираться с изолятами</li>
71 <li>Цикл жизни вашего приложения настолько короткий, что нет смысла его оптимизировать</li>
71 <li>Цикл жизни вашего приложения настолько короткий, что нет смысла его оптимизировать</li>
72 </ul><p>В противном случае -- вы можете обратить свое внимание на данный подход и пакет, который упростит вашу работу с изолятами.</p>
72 </ul><p>В противном случае -- вы можете обратить свое внимание на данный подход и пакет, который упростит вашу работу с изолятами.</p>
73  
73