Нетворкінг у Flutter додатках — про просте і складне на прикладі Tide. Частина 4: HTTP клієнт та перехоплювачі запитів з dio. Про складне

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт! Я Анна — експертка з мобільної розробки, GDE з Dart та Flutter, досвідчена розробниця мобільних додатків на Flutter.

Більшість додатків, чи то мобільні, чи то веб, чи десктоп, залежать від того чи іншого бекенда. Отже, імплементація комунікації з API є невід’ємною частиною реалізації додатку. У цій серії з шести частин представлені інструменти та підходи, які полегшують розробку комунікації з API у Flutter додатках, які ми використовуємо в Tide.

Обіцяю, буде корисно і цікаво розробникам будь-якого рівня!

Read in English

Якщо загубилися, почніть читати з початку.

Частини 3 і 4 цієї серії присвячені налаштуванню HTTP клієнта, який використовується для завантаження даних з бекенду.

Ця частина має на меті показати більш просунуті налаштування об’єкта dio.

В цій частині про складне:

  1. імітація API
  2. проксі
  3. SSL-pinning

Про просте читайте в Частині 3 цієї серії:

  1. dio HTTP клієнт
  2. перехоплювачі запитів
  3. перший API запит

На момент випуску цієї серії актуальна версія Flutter 3.0.

Приклади будуються на основі коду, створеного в Частині 3, який знаходиться під тегом part-3 у Flutter Advanced Networking GitHub репозиторії.

1. Імітація API

Окрім того, що було описано в попередній частині, ми в команді придумали ще одне цікаве застосування концепції dio перехоплювачів.

У багатьох випадках ми створюємо sociable юніт-тести, що означає, що ми тестуємо більше одного рівня абстракції за раз. Стратегія тестування в нашій компанії сама по собі є цікавою історією і може стати темою наступної серії. Але щоб швидко ознайомити вас з концепцією, наведу приклад. Погляньте на цей репозиторій:

Він отримує залежність MarvelComicsApi в конструктор. Коли викликається метод .getComic(), він просить _api завантажити та десеріалізувати відповідь, та повертає дані.

При написанні solitary юніт-тестів ми б створили заглушку MarvelComicsApi, яка б повертала замоканий обʼєкт MarvelApiResponse<MarvelPaginatedList<MarvelComic>>>. І тест для MarvelComicsRepository мав би пересвідчитися, що метод .getComics() викликає MarvelComicsApi.getComics() і повертає належні дані.

Такий тест перевірив би лише взаємодію між MarvelComicsRepository і MarvelComicsApi. А нам ще довелося б писати тести на десеріалізацію моделей MarvelComic, MarvelPaginatedList і MarvelApiResponse, а також тести для взаємодії між MarvelComicsApi і dio.

Натомість ми тестуємо їх усіх разом. Ми лише імітуємо API відповідь за допомогою dio перехоплювача. Якщо адреса запиту містить /comics, він поверне підготовлений marvelComicsApiGetComicsResponseString, який є повною копією справжньої відповіді від Marvel Comic API. В іншому випадку він дозволить виконати реальний запит, що насправді впаде в тестах:

Тепер sociable юніт-тест може перевірити взаємодію між MarvelComicsRepository, MarvelComicsApi і dio, а також десеріалізацію моделей MarvelComic, MarvelPaginatedList і MarvelApiResponse:

Об’єкт marvelComicsApiGetComicsResponseData типу MarvelPaginatedList<MarvelComic> містить дані, що відповідають вмісту marvelComicsApiGetComicsResponseString.

Чесно кажучи, ми пишемо юніт-тести навіть не для репозиторіїв, а натомість тестуємо цілі фічі додатку. Тобто, ми не створюємо такі об’єкти, як marvelComicsApiGetComicsResponseData, а замість цього використовуємо віджет-тести, щоб протестувати, що користувачі бачитимуть на екрані. Ми також не маємо окремого dio перехоплювача для кожного запиту, а натомість маємо універсальний перехоплювач, який можна конфігурувати для імітації відповідей бекенду у ​​тестах. Як я вже казала, наша стратегія тестування — це тема для іншої серії.

У такого підходу є цікавий побічний ефект. Іноді може статися, що мобільний додаток розробляється швидше, ніж необхідні API. Або дев-середовище на бекенді може бути недоступне. Або ще складніше, можливо, нам необхідно розробити інтеграцію зі стороннім сервісом, який або ще розробляється, або може також не працювати з якоїсь причини, над якою ми не маємо контролю. Щоб розблокувати себе та продовжувати розробляти код якомога ближче до продакшн версії, ми можемо використовувати такі dio перехоплювачі поза тестами, щоб імітувати реальні відповіді від API:

При такій ініціалізації dio не виконуватиме реальний запит до Marvel Comic API, щоб отримати список коміксів, а натомість повертатиме попередньо визначену відповідь. Це дуже гнучкий спосіб імітувати поведінку бекенду, включаючи сценарії з помилками.

2. Проксі

Існує кілька причин, з яких додаток мусить брати до уваги проксі-конфігурацію пристрою. Користувачі можуть мати налаштування безпеки, що взагалі забороняє програмі використовувати інтернет, якщо вона не дотримується налаштувань проксі. Команда тестувальників може захотіти протестувати додаток на локальному сервері тощо. За замовчуванням, додатки ігнорують проксі-налаштування пристрою. Ось як ми це виправляємо.

Внутрішньо dio використовує HttpClient для виконання запитів через адаптер. dio надає стандартний і зручний для розробників інтерфейс, а HttpClient є реальним об’єктом, який виконує HTTP-запити. Адаптер за замовчуванням DefaultHttpClientAdapter має метод onHttpClientCreate, що надає можливість взаємодії з внутрішнім об’єктом HttpClient. Той має функцію findProxy, під час виклику якої він зчитує налаштування проксі:

Функція _findProxy буде викликатися з кожним запитом до об’єкта dio, і саме тут внутрішній HttpClient може бути поінформований про поточне налаштування проксі. Наразі він повертає значення DIRECT, що означає відсутність проксі-налаштуваннь. Як прочитати реальні налаштування проксі з пристрою?

Ми в команді використовуємо власноруч написаний плагін для зчитування проксі-налаштувань пристрою, який включає спілкування з нативною платформою через MethodChannel. Однак для простоти цього прикладу використаємо плагін native_flutter_proxy:

Метод readProxySetting асинхронно зчитує налаштування проксі та повертає актуальне значення або null:

Зчитування проксі-налаштувань пристрою — це асинхронна операція, яка повертає Future, що робить неможливим використання цього методу безпосередньо у методі findProxy об’єкта HttpClient. Щоб подолати це обмеження, ми розробили рішення з трьох частин:

1. ProxyHolder — це сховище для налаштувань проксі в пам’яті:

2. dio перехоплювач ProxyInterceptor асинхронно зчитує проксі-налаштування пристрою за допомогою readProxySetting під час кожного запиту, та зберігає результат у ProxyHolder:

3. ProxyFinder має метод findProxy, який потім синхронно зчитує налаштування проксі з ProxyHolder:

І ProxyFinder, і ProxyInterceptor мають використовувати один і той же екземпляр ProxyHolder. Ці елементи поєднуються при створенні екземпляру dio:

В результаті метод onRequest перехоплючава ProxyInterceptor викликається перед кожним викликом до API, і зберігає поточні налаштування проксі в ProxyHolder. Метод findProxy обʼекту ProxyFinder також викликається перед кожним викликом до API, але після перехоплювача, тому можна бути впевненими, що він прочитає свіжі налаштування проксі зі спільного ProxyHolder.

Якщо немає вимоги негайно підхоплювати нові налаштуваннями проксі, ProxyHolder може повертати кешовані значення до перезапуску програми.

3. SSL-pinning

Якщо пристрій має проксі-конфігурацію, що вказує на такі інструменти, як Charles Proxy, вони зможуть відображати запити, але їх вміст не буде доступний для читання або модифікації. У цьому і полягає краса HTTPS.

Якщо буде спроба прочитати трафік за допомогою SSL-проксі, додаток видасть виключення HandshakeException, що означає помилку SSL рукостискання. Це тому, що Flutter додатки не довіряють самопідписаним сертифікатам SSL, які використовуються додатками моніторінгу веб-трафіку. Якщо є потреба дозволити читання зашифрованого трафіку за допомогою таких інструментів, їх сертифікати повинні бути внесені в список надійних сертифікатів у додатку.

Для цього необхідно завантажити рутовий SSL сертифікат додатка моніторінгу веб-трафіку у форматі PEM в каталог assets Flutter додатку:

І додати його до набору надійних сертифікатів перед першим викликом API:

Тепер є можливим перехоплення зашифрованого HTTS трафіку у додатках моніторінгу веб-трафіку, таких як Charles Proxy.

Однак насправді, для додаткового рівня безпеки, єдиний сертифікат, якому додаток повиннен довіряти, — це сертифікат сервера, з яким він збирається спілкуватися, і відмовлятися від спілкування з усіма іншими. Це називається SSL-pinning.

Оскільки Marvel Comic API, що використовується в цій серії, є публічним API, його сертифікат можна завантажити за допомогою:

І конвертувати у формат PEM за допомогою:

Процес піннінгу цього сертифікату такий самий: додати його до assets, завантажити під час старту додатку, та додати до набору надійних сертифікатів за допомогою методу setTrustedCertificatesBytes. Щоб уникнути асинхронного процесу читання з ресурсів під час запуску додатку, ми заздалегідь прочитали вміст сертифіката і зберегли його Uint8List версію у константу:

Тепер, роблячи запит до Marvel Comic API, додаток буде спілкуватися лише із сервером з цим публічним сертифікатом.

Результат

Ми застосували більш просунуті налаштування екземпляра dio, який використовується для виконання запитів до API.

Остаточна версія коду, розробленого в цій частині, знаходиться під тегом part-4 у Flutter Advanced Networking GitHub репозиторії.

Продовження у Частині 5: REST API запити з retrofit. Про просте.

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі