Нетворкінг у Flutter додатках — про просте і складне на прикладі Tide. Частина 6: REST API запити з retrofit. Про складне
Усі статті, обговорення, новини про Mobile — в одному місці. Підписуйтеся на телеграм-канал!
Привіт! Я Анна — експертка з мобільної розробки, GDE з Dart та Flutter, досвідчена розробниця мобільних додатків на Flutter.
Більшість додатків, чи то мобільні, чи то веб, чи десктоп, залежать від того чи іншого бекенда. Отже, імплементація комунікації з API є невід’ємною частиною реалізації додатку. У цій серії з шести частин представлені інструменти та підходи, які полегшують розробку комунікації з API у Flutter додатках, які ми використовуємо в Tide.
Обіцяю, буде корисно і цікаво розробникам будь-якого рівня!
Якщо загубилися, почніть читати з початку.
Частини 5 і 6 цієї серії присвячені ефективній реалізації REST API запитів.
Ця частина має на меті показати, як ми в команді налаштовуємо хедери для окремих / групи / більшості / всіх запитів до API за допомогою згенерованого коду від retrofit та dio перехоплювачів.
В цій частині про складне:
- хедери для окремих запитів
- хедери для більшості / всіх запитів
- хедери для групи запитів
- дії до / після запитів
Про просте читайте в Частині 5 цієї серії:
- простий API запит
- атрибути API запитів
- generic API відповіді
На момент випуску цієї серії актуальна версія Flutter 3.0.
Приклади будуються на основі коду, створеного в Частині 5, який знаходиться під тегом part-5 у Flutter Advanced Networking GitHub репозиторії.

1. Хедери для окремих запитів
Якщо разом із окремим API запитом необхідно надсилати заздалегідь відомі статичні хереди, його можна анотувати атрибутом @Headers
, як у рядку 10
:
Згенерований файл .g.dart
оголошує мапу _headers
з тими ж значеннями в рядку 11
і додає їх до хедерів запиту в рядку 18
:
Розповім про хитрість, що робити, якщо є потреба вказувати ці заголовки знову і знову. Dart дозволяє використовувати будь-яке const
значення як анотацію. Отже, реалізація вище повністю ідентична наведеній нижче. Тут результат виклику конструктора Headers
зберігається в const
об’єкті popularHeaders
у рядку 5
, який потім використовується як анотація в рядку 14
:
Ця реалізація генерує той самий код, що й вище.
Атрибут @Header
дозволяє задати один динамічний хедер окремого запиту. Для прикладу в рядку 10
:
згенерований файл .g.dart
використовує те саме значення хедеру в рядку 11
і додає його до хедерів запиту в рядку 15
:
Той же трюк зі створенням анотації можна застосувати до атрибута @Header
. Тут Header('header-name')
зберігається в const
полі popularHeader
, а замість нього в рядку 12
використовується анотація @popularHeader
:
Згенерований код такий самий. Тепер @popularHeader
можна використовувати в інших запитах без повторення імені хедеру.
Щоб надсилати кілька динамічних хедерів з окремим запитом, можна їх надавати один за одним, як було щойно показано, або знову ж через атрибут @Headers
, але як у рядку 10
:
Згенерований файл .g.dart
використовує параметри header1Value
і header2Value
як хедери у рядках 12
і 13
:
Атрибут @Headers
з динамічними хедерами також можна зберегти до const
анотації:
Однак, щоб цей підхід працював, назви параметрів методу мають відповідати значенням у @popularDynamicHeaders
, а про цю вимогу легко забути, якщо оголошення анотації розташовано в іншому файлі.

2. Хедери для більшості / всіх запитів
Якщо до всіх API запитів потрібно додавати одну й ту саму групу хедерів, це можна ефективно зробити за допомогою dio
перехоплювачів. Пам’ятаєте MarvelApiAuthInterceptor
з попередньої частини, який додає необхідні параметри для здійснення авторизованих запитів до Marvel Comic API? Ідея така ж сама: створити dio
перехоплювач, який додаватиме необхідні статичні хедери або хедери, значення яких можна отримати незалежно від запиту, до всіх запитів. Динамічні хедери не можуть бути додані через dio
перехоплювачі, оскільки їх значення відоме лише на момент виклику API.
Цей ExampleInterceptor
додає один статичний хедер у рядку 16
, один змінний хедер, який відрізняється для кожного запиту в рядку 19
, і один хедер, отриманий із зовнішнього джерела в рядку 21
:
Коли доданий до екземпляра dio
, він надсилає цю групу хедерів з кожним запитом. Якщо є лише кілька запитів, які не повинні містити один із хедерів, наприклад, запити /login
і /forgot-password
не потребують access-key
хедера, ExampleInterceptor
все ще можна використовувати з незначними змінами, як у рядку 10
і 16
:
Очевидно, розробники повинні не забувати оновлювати список _exceptions
з реалізацією кожного нового запиту. Цей підхід може добре працювати для малих і середніх проектів. Але ми в компанії використовуємо більш просунуту техніку.

3. Хедери для групи запитів
Ми розробляємо досить великий мобільний додаток із сотнями API викликів. Серед інших хедерів, які додаються до кожного запиту, є п’ять особливих хередів, які надсилаються з деякими або більшістю запитів до API. Вони представляють інформацію, глобально доступну в додатку, як-от ключі доступу, деякі ідентифікатори, інформацію про пристрій тощо. Для простоти назвемо їх "header1"
, "header2"
, ... "header5"
.
Усі запити до нашого API вимагають від двох до чотирьох із цих хередів. Так, наприклад, requestA
вимагає "header1"
і "header2"
, requestB
— "header2"
, "header3"
і "header5"
, requestC
— "header2"
, "header3"
, "header4"
і "header5"
і requestD
вимагає лише "header2"
і "header4"
. Тут requestA … requestD
скоріше означають типи запитів, ніж якісь конкретні запити.
Тільки уявіть, як може виглядати надання цих хередів до окремих запитів з атрибутом @Header
:
Щоб використовувати HeadersExampleApi
, розробникам доведеться знову і знову отримувати ту саму інформацію із загальнодоступних джерел:
Пам’ятаючи, що в нашому проекті сотні таких запитів requestA
, requestB
та інших, важко уявити, до якого обсягу дублювання коду призведе цей підхід. Чи не було б круто підключити dio
перехоплювачі, які додавали б хереди лише до необхідних запитів? Але як повідомити перехоплювача про те, які запити вимагають які хереди? В ідеалі перехоплювач повинен виглядати приблизно так:
Насправді, ми створили окремі перехоплювачі, призначені для кожного хедера, але для простоти прикладу всі вони можуть бути оброблені в одному перехоплювачі. Отже, яка ця <some condition>
має бути? Враховуючи масштаб нашого проекту, підхід зі списком винятків не є прийнятним рішенням. Нам був потрібен простий спосіб позначити кожен запит набором необхідних йому хередів, який був би зрозумілим для dio
перехоплювача. Перш ніж розповісти про наше рішення, познайомлю час з ще одним атрибутом — @Extra
.
Атрибут @Extra
призначений для зберігання кастомних полів. Він дозволяє разом з запитом надати додаткову Map<String, Object>
, яку пізніше можна прочитати в dio
перехоплювачах. Для атрибута @Extra
в рядку 9
:
згенерований файл .g.dart
містить таке ж значення в рядку 10
:
Тепер його можна прочитати в dio
перехоплювачі, як у рядку 8
:
Ми також очищаємо extra
мапу в рядку 16
, щоб жоден інший перехоплювач не обробляв ті самі дані.
Дотримуючись цього підходу, ми розділили всі запити до API на групи на основі хедерів, які їм потрібні, і створили спеціальні анотації для кожної групи:
Коли така анотація застосовується до запиту, як у рядках 9
і 13
:
згенерований файл .g.dart
містить скопійовані значення, як у рядках 10
і 20
:
Тепер їх можна прочитати в dio
перехоплювачі, щоб вирішити, чи додавати хедери:
А користувачі HeadersExampleApi
вільні від надання нерелевантної (з їх точки зору) інформації і можуть зосередитися на передачі лише тих даних, які мають значення:
Нагадаю, що немає відповідності один до одного між анотацією з типом запиту та самим запитом. Анотації requestAType, … requestDType
можна застосувати до будь-якого retrofit запиту, і це призведе до додавання необхідних хередів, якщо AppendHeadersInterceptor
приєднано до екземпляра dio
. Наприклад, тут запит .getComics()
з MarvelComicsApi
анотується атрибутом @requestAType
у рядку 9
:
В результаті разом з запитом буде відправлено "header1"
і "header2"
.

4. Дії до / після запитів
Оскільки наша програма дозволяє керувати банківськими рахунками користувачів, деякі операції є особливо чутливими до безпеки, й вимагають від користувачів вводити свій PIN-код або використовувати біометричні дані. Це надає нам можливість отримати доступ до зашифрованого сховища та зчитати деякі дані, щоб передати їх на сервер. Для простоти скажімо, що ми наївно запитуємо користувачів, чи погоджуються вони поділитися секретними даними за допомогою простого «Так / Ні» діалога:
Очевидна реалізація полягає в тому, щоб викликати .getSecretData()
перед викликом кожного запиту API, чутливого до безпеки:
Однак у цього підходу є той самий недолік, що й у попередньому розділі: дублювання коду під час реалізації кожного чутливого до безпеки запиту та опікування нерелевантною, з точки зору користувача API, інформацію. Рішення також те саме: створити dio
перехоплювач і знайти спосіб повідомити йому, яким запитам має передувати діалогове вікно дозволу користувача.
Ви можете подумати: «Зачекай, я знаю рішення: використай анотацію @Extra
та додай інший маркер до необхідних запитів!». Ми також так думали, але виявляється, що поверх retrofit запиту можна додати лише один атрибут типу @Extra
. Якщо їх кілька, як у рядках 10
і 11
:
у створеному файлі .g.dart
буде використаний лише перший:
Тож нам довелося використати інший атрибут, який міг би анотувати retrofit запит та впливати на згенерований код — атрибут @Headers
. Перевага використання атрибута @Extra
полягала в тому, що навіть якщо до екземпляра dio
не приєднано жодного перехоплювача, і ніхто не очистить мапу _extra
, мати в ній якісь дані не шкідливо, оскільки насправді вони не використовуються, коли запит надсилається до бекенду. З @Headers
, якщо жоден перехоплювач не прослуховує dio
і не замінює маркуючі хедери чимось біль значущим, вони будуть відправлені «як є» і можуть викликати непорозуміння з бекендом.
Отже, ми створили нову анотацію:
І dio
перехоплювач, який буде чекати та обробляти такий хедер:
У рядку 14
він визначає, чи мапа з хедерами містить хедер зі спеціальним значенням secureActionHeader
. Якщо так, перехоплювач запитує секретні дані в рядку 16
. Якщо запит був успішним, дані додаються як значення хедера security-header
, і запит продовжує виконуватись в звичайному режимі. В іншому випадку запит до API відхиляється в рядку 20
. Також ми очищаємо нерелевантне значення хедера в рядку 15
.
Тепер @secureAction
можна використовувати над retrofit запитом:
А користувачі API можуть забути про деталі його реалізації:
Анотацію @secureAction
можна використовувати над будь-яким retrofit запитом, тому тут вона анотує метод .getComics()
:
Тепер кожен раз, коли користувач оновлює список коміксів, він бачить діалог з підтвердженням.
Той самий підхід може бути застосований для дій, що треба виконувати після запитів. Все залежить від того, чи код в перехоплювачі виконується до виклику super.onRequest(options, handler)
чи після.

Результат
Ми впровадили різні способи налаштування викликів до API на рівні retrofit для окремих запитів, а також глобально в dio перехоплювачах.
Остаточна версія коду, розробленого в цій частині, знаходиться під тегом part-6 у Flutter Advanced Networking GitHub репозиторії.

Заключні слова
Як і обіцяла, ми розробили програму, яка відображає список Marvel коміксів, отриманих з Marvel Comic API. Метод .getComics()
з MarvelComicsApi
використовується для отримання списку коміксів, а результат відображається в дуже простому інтерфейсі:
Остаточну версію коду з повністю реалізованим Flutter додатком можна зайти у Flutter Advanced Networking GitHub репозиторії.
Інструменти та підходи, описані в цій серії, максимально наближені до тих, що ми створили в Tide під час розробки наших мобільних додатків на Flutter. Якщо ви так само як і ми в захваті від цієї технології, відвідайте нашу сторінку з вакансіями, ми наймаємо мобільних Flutter розробників у різних країнах, в тому числі й віддаленно в Україні. Ми з нетерпінням чекаємо зустрічі з професіоналами-однодумцями!
13 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів