Изучение пользовательского интерфейса, управляемого сервером, в SwiftUI и Jetpack Compose

Если вы уже программировали мобильное приложение, вы уже должны быть знакомы с макетом экрана, создаваемым в такой структуре, как файлы .xib, .storyboard, .xml, или даже в самом коде Swift, Kotlin, Dart или JavaScript. Мы можем создавать эти экраны статически, непосредственно в комплекте приложения, но бывают случаи, когда это необходимо для получения самой последней информации (например, каталога или истории) или выполнения действия, требующего проверки (например, аутентификации или даже покупки этой закуски для доставки). — в этих случаях у нас нет другого выбора, кроме как полагаться на бэкенд.

Бэкэнд — это вся экосистема, которая хранит данные и предоставляет приложения, содержащие бизнес-правила, которые будут использоваться клиентами — в нашем случае это мобильное приложение. Эти приложения, в свою очередь, отправляют запросы к серверу для извлечения данных, их форматирования, создания макета и отображения его на экране для клиентов.

С интерфейсом, управляемым сервером (также известным как пользовательский интерфейс, управляемый сервером), игра немного меняется. Сервер не только возвращает данные, но и возвращает как будет построен экран. Его состав, элементы, компоненты и формат, все определено.

Но… какая от этого польза? Давайте проверим это.

Определение

Одним из способов визуализации является представление компонентов вашего приложения (таких как кнопки, карточки или форматированный текст) в виде блоков LEGO, в то время как сервер предоставит инструкции по сборке приложения. Существует несколько способов создания пользовательского интерфейса, управляемого сервером, но основная идея основана на следующих моментах:

  • Служба, отвечающая за создание структуры экрана или руководства по эксплуатации с соответствующими конфигурациями, данными и действиями.
  • Приложение, которое выполняет запросы экрана и способно собирать компоненты с возвращенными конфигурациями.

Архитектура

Размышляя об архитектуре, ориентированной на микросервисы, мы можем представить пример только с тремя основными компонентами:

  • Приложение: само приложение, его пользовательский интерфейс. Это также может быть, например, мобильное приложение или веб-система.
  • Бизнес-служба: одна или несколько служб, отвечающих за бизнес-правила, базы данных, состояния, эксперименты и т. д.
  • Служба пользовательского интерфейса: центральный организатор, отвечающий за получение бизнес-информации из бизнес-служб и составление инструкций по эксплуатации экрана. Вы также можете определить это как BFF, специализирующийся на пользовательском интерфейсе.

Процесс открытия управляемого сервером экрана может быть реализован примерно так:

Начнем с простого, давайте подумаем о простом текстовом компоненте, размер которого можно настроить. Затем наша служба пользовательского интерфейса должна вернуть этот компонент в «руководстве по эксплуатации» с ответом вроде:

{
  "text": "Hello, world!"
  "textSize": 12
}

Где содержимое находится в text, а настройка размера шрифта находится в свойствах textSize. Поэтому соответствующие приложения должны знать, как настроить текстовый компонент с помощью этой информации. Для этого мы создали ServerDrivenUIText, построенный на основе структуры словаря/карты:

iOS/SwiftUI

import SwiftUI
import Foundation

struct ServerDrivenUIText: View {
  @State var data: [String: Any] = [:]
  
  var body: some View {
    switch data.isEmpty {
    case true:
      ProgressView()
    case false:
      buildUI(fromData: data)
    }
  }
  
  func buildUI(fromData json: [String: Any]) -> some View {
    guard let text = json["text"] as? String, 
          let textSize = json["textSize"] as? CGFloat else {
      return Text("Error")
    }
    return Text(text)
      .font(.system(size: textSize))
  }
}

Android/Написать

import androidx.compose.foundation.Text
import androidx.compose.foundation.layout.Column
import androidx.compose.material.ProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp

@Composable
fun ServerDrivenUI() {
    val data = mutableStateOf(mapOf<String, Any>())
    when (data.value.isEmpty()) {
        true -> ProgressIndicator(modifier = Modifier.padding(16.dp))
        false -> buildUI(data = data.value)
    }
}
@Composable
fun buildUI(data: Map<String, Any>) {
    Column {
        val text = data["text"] as? String ?: return@Column Text("Error", Modifier.padding(16.dp))
        val textSize = data["textSize"] as? Int ?: 14
        Text(text, 
            modifier = Modifier.padding(16.dp), 
            fontSize = textSize.toDp())
    }
}

Конечно, загрузку простого текста можно было бы еще упростить. Но наличие такой настройки, как textSize, уже позволяет вносить изменения в макет приложения без необходимости отправлять новую версию в магазины, ждать одобрения и полагаться на добрую волю пользователя для обновления приложения. Это потенциально самое большое преимущество этой архитектуры.

Итак, это все?

Да! В повседневной жизни мы имеем дело с экранами гораздо более сложными, чем просто текстовая составляющая. Вот почему интересно иметь хорошо структурированную и настраиваемую библиотеку компонентов; достаточно зрелым, чтобы иметь возможность создавать более сложные компоненты и даже целые экраны или потоки.

При хорошей структуре компонентов в приложении, если необходимо реализовать, например, начальный экран аутентификации, с Server-Driven мы могли бы получить что-то вроде:

{
  "name":"Home Screen",
  "elements":[
    {
      "type":"column",
      "children":[
        {
          "type":"text",
          "text":"Welcome to the app!",
          "fontSize":24,
          "color":"#000000"
        },
        {
          "type":"text",
          "text":"Please log in to continue",
          "fontSize":18,
          "color":"#777777"
        },
        {
          "type":"row",
          "children":[
            {
              "type":"button",
              "text":"Log in",
              "backgroundColor":"#4CAF50",
              "color":"#FFFFFF",
              "onPress":"loginFunction"
            },
            {
              "type":"button",
              "text":"Sign up",
              "backgroundColor":"#3F51B5",
              "color":"#FFFFFF",
              "onPress":"signupFunction"
            }
          ]
        }
      ]
    }
  ]
}

И вуаля! Нет необходимости отправлять новую версию в магазин, чтобы экран выглядел так, с заданными настройками (размер шрифта, цвета) и уже можно было проводить A/B-тестирование (например: с расположением кнопок или другим текстом ) без добавления ни одной строки кода для флага функции в пакете приложения. И поскольку все это обрабатывается бэкэндом, вы можете устанавливать экспериментальные группы непосредственно на основе запроса пользователя. Разве это не круто?

Вы можете подумать о чрезвычайно важном пробеле: "Хорошо, мое приложение может открываться, но как мне открыть этот новый экран, поскольку мне нужно добавить код для отправки с одного конкретного экрана? другому? Есть ли способ динамически открывать эти экраны?» Выход всегда есть, падаван, но это тема для другой статьи.

Преимущества

Когда дело доходит до преимуществ и преимуществ, имейте в виду, что они различаются в зависимости от проблемы, которую вы хотите решить, и/или контекста, в котором он используется. Однако, в целом, некоторые моменты, которые мы можем выделить:

  • Быстрые обновления и доставка. Приложение можно обновлять, не загружая и не устанавливая новую версию. Это действительно ценно, когда речь идет об исправлении критических ошибок или новых функциях. Обычно процесс обновления приложения может занять некоторое время, поскольку он зависит от проверки магазина, поэтапного развертывания и загрузки новой версии пользователем. Этот процесс может занять несколько недель, в то время как с помощью Server-Driven это может произойти за один день.
  • Обеспечить безопасность: при использовании этого подхода мы позволяем серверу проверять информацию перед ее отправкой в ​​мобильное приложение, что помогает обеспечить надежность данных и защиту от атак. Это особенно актуально в приложениях, которые имеют дело с конфиденциальными данными, такими как финансовые транзакции или личная информация. Таким образом, если в определенной версии есть серьезная брешь в системе безопасности, ваш сервер может обнаружить проблемную версию и заблокировать доступ, возвращая индивидуальный интерфейс.
  • Удобно для экспериментов: проводить A/B-тестирование стало проще и эффективнее. Это означает, что вы можете тестировать разные версии пользовательского интерфейса с разными группами пользователей, не обновляя мобильное приложение флагом функции. Это возможно, потому что пользовательский интерфейс динамически загружается сервером.
  • Персонализация и доступность. Мы можем персонализировать пользовательский интерфейс для различных групп пользователей в зависимости от их потребностей, таких как местоположение, язык или устройство.
  • Повторное использование кода. Имея единый «источник достоверности» для создания определенного экрана, процесс разработки выполняется только один раз, и этот код уже можно распространять на несколько платформ.

Обеспокоенность

Как и любая технология, Server-Driven UI не является идеальным подходом. Также важно учитывать следующие проблемы, прежде чем принимать решение о внедрении в экосистему вашего приложения:

  • Задержка и производительность. Поскольку мобильному приложению необходимо взаимодействовать с сервером для получения данных и обновлений, при отображении пользовательского интерфейса будет возникать задержка. Это связано с тем, что, помимо времени отклика сервера, необходимо обратить внимание на обработку и память. Эти проблемы могут вызвать проблемы в приложениях, требующих быстрых ответов, таких как игры или приложения для обмена сообщениями.
  • Зависимость от подключения к Интернету. Поскольку мобильному приложению необходимо обмениваться данными с сервером для получения данных и обновлений, оно может работать только при наличии установленного подключения к Интернету. Это может быть проблемой в ситуациях, когда пользователь имеет ограниченное покрытие сети или у него нет доступа в Интернет, или в приложениях, разработанных с архитектурой, ориентированной на автономный режим.
  • Стоимость. Размещение и запуск серверного решения пользовательского интерфейса может стать дорогостоящим, поскольку требует дополнительных серверных ресурсов и возможностей подключения. Имейте в виду, что для каждого клиента, открывающего экран, у нас есть как минимум еще один запрос, сделанный на сервере.
  • Сложность. Настройка и обслуживание серверной инфраструктуры пользовательского интерфейса может быть сложной задачей и требует специальных знаний. У вас будет дополнительная область для создания инструментов, мониторинга и отладки. Еще один момент, который следует учитывать, заключается в том, что для каждого нового компонента, добавленного в библиотеку пользовательского интерфейса, который вы хотите использовать с управляемым сервером, вам, возможно, придется добавить поддержку на обоих фронтах: на стороне сервера (определение контракта и заполнение данными) и на стороне сервера. мобильная сторона (разбор и создание актуального компонента).

Итак, является ли это окончательным решением для мобильных приложений?

Нет. Пользовательский интерфейс, управляемый сервером, — это подход, при котором сервер отвечает за создание и отправку интерфейса клиенту, который может быть мобильным или веб-приложением. И это мощно, потому что оно может принести несколько преимуществ, таких как гибкость обновлений и критических исправлений, экспериментирование и персонализация; но есть также такие проблемы, как задержка и производительность, зависимость от подключения к Интернету и сложность.

Но помните, что серебряной пули не существует, и у каждой методологии есть свои компромиссы. Прежде чем выбрать какой-либо подход, важно оценить проект, контекст и бизнес-требования. Таким образом, это должно быть полностью на вашем столе, когда вы разрабатываете решение.

Спасибо за прочтение.