Перейти к содержимому

Прагматичный networking в iOS с Rage

Дисклеймер: По состоянию на 2020 год библиотека не поддерживается мной и плохо поддерживается для использования с новыми версиями Swift. Но мне нравится идея библиотеки, которая всё ещё интересна.

Этот пост о Rage, это наша библиотека для создания абстракции над реализацией API в iOS.

Мало какие мобильные приложения в наши дни не имеют API. Поэтому нам приходится создавать сетевой слой каждый раз для каждого нового проекта.

С точки зрения мобильной разработки мы видим, насколько по-разному реализован каждый бэкенд. Это в значительной степени зависит от выбранных серверных технологий, таких как строгие и предсказуемые Java и .NET, гибкие и разные Node.js, Ruby и Python. В то же время, когда мы имеем дело с мобильными приложениями, у нас всегда один и тот же технологический стек, и мы хотим иметь понятный и предсказуемый код, который выглядит одинаково для каждого проекта.

Состояние мира

Когда вы переходите в царство iOS-разработки из мира Android, вас неизбежно удивят паттерны, которые здесь довольно популярны. Для сетевых целей есть довольно хороший URLSession от Apple, но многие проекты всё ещё используют Alamofire, который является преемником AFNetworking. Есть Moya, который является более высоким уровнем абстракции над Alamofire. Moya приличная библиотека, и мы использовали её в некоторых проектах. Она хорошо документирована и протестирована, но её синтаксис с злоупотреблением enum — это то, с чем мы не смогли жить. Описание API мгновенно превращается в тонны switch-case по одному и тому же перечислению. Каждый параметр одного запроса описан в своём собственном месте, и это действительно сбивает с толку, когда вы пытаетесь понять всё об одном запросе. Мы принимаем понятность идеологии Moya, но это влияет на нашу продуктивность. Иногда кажется, что Moya загнала себя в угол.

Ключевые бузворды мира Android — это OkHttp, Retrofit и Moshi. Никто не сомневается в том, почему они так хороши. Естественно, мы хотим таких же вещей и в iOS. В Swift нет annotation processing, к которому мы привыкли в Java/Kotlin, и поэтому здесь непросто сделать прямые аналоги. Предположительно, Swift 4 решает проблему с JSON. OkHttp предлагает хороший builder-стиль синтаксис для спецификации сетевых запросов. Retrofit предлагает способ описать список запросов, как сериализовать/десериализовать данные и какой http-клиент использовать для выполнения запросов.

Rage

Rage делает нечто похожее. Это наша библиотека для того, чтобы сделать спецификацию API более читаемой и понятной. Мы стремимся делать меньше ошибок и нацелены на высокие возможности кастомизации. Именно поэтому мы создали Rage.

Мы дали библиотеке это имя из-за всех эмоций, с которыми нам приходилось иметь дело при реализации API в iOS-приложениях до появления этой библиотеки.

По сути, мы можем представить сетевой слой как список описаний запросов и информацию о том, как делать запросы в целом (то есть клиент). Клиент — это то, что выполняет запросы. Он знает общую информацию обо всех запросах. Каждый запрос имеет свои специфические параметры.

На данный момент у нас есть следующие параметры для клиента:

  • Base URL
  • Base ContentType
  • URLSessionConfiguration
  • Некоторые плагины, например logger
  • Заголовки для всех запросов
  • Описание процесса авторизации запросов

И вот что у нас есть для каждого конкретного запроса:

  • URL
  • ContentType
  • HTTP Method
  • Relative Path
  • Заголовки
  • Query-параметры (key-value, no-value, array)
  • Path-параметры
  • Требует ли этот запрос авторизации

Мы часто используем вспомогательные методы для особых случаев:

  • Запросы с телом (создание тела из Data, String, любого Codable-объекта)
  • Multipart-запросы (создание multipart-запросов никогда не было проще)
  • FormUrlEncoded-запросы (создание тела со списком key/value параметров)

Сериализация и десериализация также могут быть интегрированы в процесс выполнения запроса. Наша цель — работать со строго типизированными объектами, а не с адом словарей, который мы видим во многих проектах. Честно говоря, мы ненавидим эти [String: Any?] вещи. Теперь в Swift 4 у нас есть Codable, поэтому мы можем просто передать Codable-объект в теле запроса, и он будет сериализован в json. Мы можем указать возвращаемый тип Codable для запроса и получить десериализованный объект из json.

По сравнению с Moya, поток в Rage очень прямолинейный. Каждый запрос заключён в свою собственную функцию, где описаны все параметры. Одна из наших ключевых фич — это, безусловно, поддержка RxSwift, что делает использование Rage ещё более выгодным.

Вот как это выглядит в простом случае

class GithubAPI {

    let client: RageClient

    init() {
        client = Rage.builderWithBaseUrl("https://api.github.com") // Создаём builder клиента
        .withContentType(.json) // Устанавливаем ContentType для всех запросов на application/json
        .withPlugin(LoggingPlugin(logLevel: .full)) // Включаем логирование
        .build()
    }

    func getOrgRepositories(org: String) -> Observable<[GithubRepository]> {
        return client.get("/orgs/{org}/repos") // Настраиваем http-метод и path запроса
        .request() // Создаём простой запрос с данной конфигурацией
        .path("org", org) // Добавляем параметры к созданному запросу
        .executeObjectObservable() // Выполняем запрос, десериализуем json-ответ в массив GithubRepository и возвращаем как Observable
    }

    func getContributors(repo: String, org: String) -> Observable<[GithubUser]> {
        return client.get("/repos/{owner}/{repo}/contributors")
        .request()
        .path("owner", org)
        .path("repo", repo)
        .executeObjectObservable()
    }

    func getOrgInfo(org: String) -> Observable<GithubOrganization> {
        return client.get("/orgs/{org}")
        .request()
        .path("org", org)
        .executeObjectObservable()
    }

}

Как видите, это выглядит очень просто для использования во внешнем коде. Это просто функция getOrgInfo("gspd-mobi"), которая вызывается в вашей бизнес-логике как любая другая функция в вашем коде.

Сложный случай выглядит не сильно иначе. Хотите добавить query-параметр, path-параметр, json-тело из Codable-объекта в один запрос? Не проблема. Всего три дополнительных вызова функций в этой цепочке запросов. Хотите использовать Rage просто для создания запроса для последующего использования? Не проблема, функция .rawRequest() для вас.

Rage не заставляет разработчиков использовать какой-либо паттерн архитектуры; по сути, это просто удобный способ описать запросы.

Библиотека построена поверх URLSession от Apple с использованием микробиблиотеки Result для инкапсуляции логики Content/Error для случаев использования без RxSwift.

Посмотрите

Каждый может проверить, что такое Rage. Установите её через CocoaPods или используйте код с Github. Но, пожалуйста, не используйте её в продакшене до версии 1.0.0. Она ещё недостаточно стабильна, и её API может меняться от версии к версии без обратной совместимости. Мы используем её во всех наших проектах и учимся на своих ошибках, мы выросли до понимания того, какие новые функции нам нужно добавить, какие недостатки имеют некоторые старые решения и как библиотека должна быть обновлена на основе наших потребностей. Ей уже 2 года, и мы сейчас действительно близки к выпуску стабильной версии. Мы успешно использовали Rage во всех наших приложениях с момента выпуска её начальной версии, и реализовывать API в iOS было одно удовольствие. Также доступна документация, которая содержит всю базовую информацию о возможностях библиотеки. Кроме того, есть также пример проекта, который позволит вам увидеть Rage в действии.