Нетворкінг у Flutter додатках — про просте і складне на прикладі Tide. Частина 4: HTTP клієнт та перехоплювачі запитів з dio. Про складне
Усі статті, обговорення, новини про Mobile — в одному місці. Підписуйтеся на телеграм-канал!
Привіт! Я Анна — експертка з мобільної розробки, GDE з Dart та Flutter, досвідчена розробниця мобільних додатків на Flutter.
Більшість додатків, чи то мобільні, чи то веб, чи десктоп, залежать від того чи іншого бекенда. Отже, імплементація комунікації з API є невід’ємною частиною реалізації додатку. У цій серії з шести частин представлені інструменти та підходи, які полегшують розробку комунікації з API у Flutter додатках, які ми використовуємо в Tide.
Обіцяю, буде корисно і цікаво розробникам будь-якого рівня!
Якщо загубилися, почніть читати з початку.
Частини 3 і 4 цієї серії присвячені налаштуванню HTTP клієнта, який використовується для завантаження даних з бекенду.
Ця частина має на меті показати більш просунуті налаштування об’єкта dio.
В цій частині про складне:
- імітація API
- проксі
- SSL-pinning
Про просте читайте в Частині 3 цієї серії:
- dio HTTP клієнт
- перехоплювачі запитів
- перший API запит
На момент випуску цієї серії актуальна версія Flutter 3.0.
Приклади будуються на основі коду, створеного в Частині 3, який знаходиться під тегом part-3 у Flutter Advanced Networking GitHub репозиторії.
1. Імітація API
Окрім того, що було описано в попередній частині, ми в команді придумали ще одне цікаве застосування концепції dio
перехоплювачів.
У багатьох випадках ми створюємо sociable юніт-тести, що означає, що ми тестуємо більше одного рівня абстракції за раз. Стратегія тестування в нашій компанії сама по собі є цікавою історією і може стати темою наступної серії. Але щоб швидко ознайомити вас з концепцією, наведу приклад. Погляньте на цей репозиторій:
Він отримує залежність MarvelComicsApi
в конструктор. Коли викликається метод .getComic()
, він просить _ap
i завантажити та десеріалізувати відповідь, та повертає дані.
При написанні 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. Про просте.
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів