TL; DR. Серверы могут определять, как приложение должно выглядеть и вести себя, будут некоторые авторы, которые будут определять и создавать потоки для пользователей. Используя идеи атомарного дизайна, примененные к библиотекам, таким как React, Compose, SwiftUI, Flutter, и сладкую смесь с такими технологиями, как Firebase / GraphQL, это можно применить к любому приложению. Клиент запрашивает page, используя Context, и сервер дает ответ, в котором вы можете отобразить все виджеты, содержащиеся на этой странице.

Некоторая предыстория

Как веб-разработчик в Matter Supply co, большая часть наших разработок была сосредоточена на создании опыта, которым было бы легко управлять, обновлять и изменять. Создав несколько приложений, которые повысили роль «автора» с помощью CMS, и проверив преимущества библиотек пользовательского интерфейса, таких как Compose, SwiftUI, React и Flutter, я решил взять эти идеи и попытаться объединить их таким образом, чтобы создание приложений, которые могут быть мобильными или сетевыми. В качестве примечания, этот пост не включает в себя так много кода, и эти концепции могут быть применены к интерфейсным приложениям, включая веб-/ мобильные технологические стеки, просто для удовольствия я буду использовать Kotlin, но вы можете легко это перевести. Если вы хотите продемонстрировать это в конкретном сценарии использования, хлопните в ладоши, и мы поговорим позже.

За последние два года опыта работы с веб-приложениями я смог поделиться некоторыми идеями о том, как сервер может определять, как должен работать пользовательский интерфейс, на этом опыте я работал с разными CMS, которые помогли мне сделать «серверную». , без кодирования. Команда смогла создать несколько компонентов пользовательского интерфейса, создать для них тесты, использовать тестирование моментальных снимков, создать набор инструментов пользовательского интерфейса, позволить «авторам» создавать страницы и соединения / действия. Итак, как это сделать в мобильном приложении? Давай узнаем это.

Есть много компаний, у которых есть некоторые подходы, похожие на то, что я затронул в этом посте, одна из них - Airbnb, пожалуйста, просмотрите такие вещи, как Lona Components, Epoxy и Showkase, которые движутся в этом направлении.

Некоторые ингредиенты для приготовления по этому рецепту.

Несколько лет назад я говорил о компонентах пользовательского интерфейса об атомарном дизайне в качестве основы для создания расширяемого пользовательского интерфейса, а также, когда я работал над Pager Inc, мы реализовали очень базовую версию этого в Android SDK от Pager, в основном с использованием Java и обычных представлений.

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

В пейджере мы начали с создания модуля с такой структурой (или похожей):

Но… где атомы, молекулы и все такое… ну, за эти годы я узнал, что использование другого языка может немного улучшить ситуацию. Определив границы того, как разделять ваши взгляды и как их составлять, если это ясно и имеет «естественный» язык.

«Уловка» заключается в том, чтобы дать вашей команде возможность быстро мыслить, им нужно составить пользовательский интерфейс с гибкими компонентами многократного использования, чтобы использовать их повсюду, я имею в виду использование «одних и тех же правил» для всего вашего приложения. Но что, если мне нужно внести небольшие изменения для одного элемента на странице / экране, вот где возникла концепция, которую я называю Feature based components.

Это концепция, которую я узнал от друга несколько лет назад, работая над HugeInc от близкого друга. По сути, концепция связана с созданием настраиваемых компонентов для вашей текущей функции пользовательского интерфейса на основе меньшего компонента, вам необходимо использовать различные методы, такие как перенос, расширение или компоновка, чтобы дать контекст, который он используется в вашей текущей функции. Если у вас есть InfiniteList компонент, вы можете расширить его, чтобы создать FeedList компонент, который может расширить исходное представление и предоставить весь контекст для конкретного варианта использования. Это создаст уникальный блок для вашей рабочей функции, вы не собираетесь портить другие представления, и это позволит вам иметь четкие модели.

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

Модуль компонентов пользовательского интерфейса

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

Если вам нужно вдохновение, чтобы проверить эти проекты Figma, и, возможно, хорошим упражнением будет попытаться преобразовать один из них в код.

Имея эти строительные блоки сейчас, вы можете начать думать обо всех возможностях, в моем случае возможности заключались в том, чтобы иметь несколько модулей / приложений, имеющих одинаковые стандарты, а не создавать все заново.

Подумайте, что у нас может быть такая библиотека, как Material UI, но с правилами, используемыми нашим приложением, код будет легче читать, меньше изменений будет интегрировано с каждым запросом на перенос, меньше ошибок и несоответствий.

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

Хотели начать сейчас? Начните с создания элементов типографики, таких как Body, Eyebrow, H1, H2, H3, Bolds, RichTexts, а также кнопки (основные, второстепенные и т. Д.) И элементы формы. У вас могут быть действительно настраиваемые элементы, такие как компоненты Skeleton, но ваша работа здесь заключается в том, чтобы иметь большое количество компонентов, которые позволят вам наиболее легко описать свой пользовательский интерфейс. Вы можете проверить некоторые подходы, такие как Carbon’s Design System от IBM, чтобы понять, как разделить ваши взгляды (я расскажу об этом в следующем примере, оставайтесь на связи).

Компоненты функции

Это довольно сложно, потому что я вижу, что никто не говорит об этом или о чем-то подобном. Давайте представим, что у нас есть старомодный компонент (кнопка), который используется в нашей функции входа в систему, и мы можем использовать его как есть, компонент может быть настолько простым, что мы можем импортировать его и использовать без изменений. Как обычно, появляется новое определение, и теперь вашим пользователям нужно ввести адрес электронной почты. Нажмите на кнопку, кнопка изменит свое содержимое на значок загрузки, когда электронное письмо будет подтверждено, вы измените текст на что-то новое, чтобы позволить пользователь знает, что пароль требуется сейчас. Возможно, не каждому Button понадобится это новое поведение (и если это что-то необходимое для всего, нам нужно переместить весь новый код в базовый компонент).

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

Другой наиболее распространенный пример - использование RecyclerView, он настолько универсален, что совершенно нормально иметь что-то вроде этого в нашем коде:

Это нормально, но, на мой взгляд, не самое лучшее, не готово к изменениям, не готово к адаптации, это мое предложение в качестве рефакторинга.

Как вы заметили, я использовал соглашение для своего кода, в ссылках на компоненты пользовательского интерфейса используется суффикс View, Form, Button, Field или что-то еще, указывающее на то, что это представление, иногда вы найдете переменные с тем же именем, что и компонент пользовательского интерфейса, и вам нужно знать, что есть что.

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

В конце концов, Feature component - это просто компонент, который расширяет один или несколько базовых строительных блоков пользовательского интерфейса, чтобы позволить их использование в определенной и конкретной ситуации.

Итак, если Feature components - это способ создания элементов пользовательского интерфейса для определенного контекста, зачем нам нужны модули? Поскольку модули являются элементами, которые добавляют эти строительные блоки и при необходимости добавляют дополнительную функциональность, как и в случае с компонентами и компонентами функций, этот уровень может быть реализован как смесью базовых блоков и расширенных. Идея модуля состоит в том, чтобы создать раздел, который ведет себя определенным образом, в случае экрана входа в систему модуль входа в систему будет отвечать за обработку потока входа в систему (а не потока данных), а в случае списка каналов, модуль может включать вид сбоку, который будет отображаться на планшетах с шаблоном "основной / подробный". Считает, что модуль - это то, что вы помещаете на экран, у которого есть поведение, анимация, но не потоки данных (они будут определены в следующем разделе).

Данные"

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

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

Если мое приложение запускается, мы можем запросить сервер, который дает нам дом, приветствие или логин.

Request: {
  path: "/home",
  context: {
    user: null,
    language: "en/US", 
    device: "Galaxy 3 very old", 
    os: { 
      name: "Android", 
      version: "2.1", 
    }, 
    app: {
      version: "1.2.3", 
      type: "beta",
    },
  },
}
Response: {
  modules:[
    {
      name: "Login",
      params: {
        labels: {
          username: "Let's begin with your username",
          password: "Shhhh nobody needs to see this",
          action: "Click me to Login"
        },
      },
      actions: {
        loginClick: {
          moveTo: null,
        },
        registerClick: {
          moveTo: "/register",
        },
        forgotClick: {
          moveTo: "/forgot",
        },
      },
    },
  ]
}

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

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

Request: {
  path: "/home",
  context: {
    user: { 
      token: "abcdef", 
      id: "1234567890", 
      name: "Juancho",
    },
    language: "en/US", 
    device: "Galaxy 3 very old", 
    os: { 
      name: "Android", 
      version: "2.1", 
    }, 
    app: {
      version: "1.2.3", 
      type: "beta",
    },
  },
}
Response: {
  modules:[
    {
      name: "NavBar",
      params: {...}
    },
    {
      name: "FeedList",
      params: {...}
    },
    {
      name: "BottomNavigation",
      params: {...}
    },
  ]
}

Эти запросы / ответы также могут содержать некоторые заголовки для предоставления дополнительных указаний к состоянию приложения, например, если вы используете OTP.

Флаги функций + A / B-тестирование

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

Используя предыдущий пример запроса / ответа, мы можем нарисовать что-то вроде этого:

Request: {
  path: "/home",
  context: {
    user: { 
      token: "abcdef", 
      id: "1234567890", 
      name: "Juancho",
      location: "California",
    },
    language: "en/US", 
    device: "Galaxy 3 very old", 
    os: { 
      name: "Android", 
      version: "2.1", 
    }, 
    app: {
      version: "1.2.3", 
      type: "beta",
    },
  },
}
Response: {
  modules:[
    {
      name: "NavBar",
      layout: "light",
      params: {...}
    },
    {
      name: "Promotions",
      layout: "full-page",
      params: {...}
    },
    {
      name: "FeedList",
      params: {...}
    },
    {
      name: "BottomNavigation",
      params: {...}
    },
  ]
}

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

Забавная история, в Matter Supply Co мы закончили какое-то приложение с таким подходом, CMS поставила нам плохую точку в новостях проекта. В CMS было ограничение на количество ответов GraphQL, поэтому она была готова предоставить нам 20 различных типов, поэтому было сложно иметь разные типы, а нам нужно было так много типов. Долгосрочным решением было добавить этот модный параметр макета, который позволил нам создавать более эффективные ответы.

Представление

Конечно, производительность - это то, что здесь следует учитывать, и две концепции - это те, которые дадут нам приятный опыт: Cache и PreFetch, мы можем очень упрощенно объяснить здесь, сказав, что идея состоит в том, чтобы запомнить ответы от сервера и получение информации оттуда раньше. Мы можем удалить информацию из ответа на нашу страницу и проверить связанные ссылки, и мы можем загрузить эту информацию до того, как это потребуется, а затем, когда мы перейдем на эту страницу, мы сделаем то же самое. Поскольку информация хранится в кеше, она вам больше не понадобится, если срок действия кеша не истечет. Поэтому, имея это в виду, вам нужен только один запрос на страницу, запрос кэшируется и предварительно загружается, но здесь все еще есть некоторое снижение производительности, мы так сильно зависим от диска и парсера.

Если описание представления и кеш слишком велики, процесс займет некоторое время, возможно, это можно улучшить, используя другую систему структуры, отличную от простого кеширования ответа JSON, возможно, что-то вроде Protobuffer будет лучше, но как только я Я пишу это, не знаю, как это улучшить (идеи приветствуются).

Заключение

У меня есть несколько идей, которые позволят вам принять или, по крайней мере, попытаться представить, как это может работать для вашего приложения в ближайшем будущем. Такой подход позволит вам создавать разные экраны или изменять приложение, не отправляя пользователям новый выпуск. Есть элементы, которые вы должны принять во внимание при этом, например, где лучше всего загружать информацию о ваших страницах, как вы создаете кеш для них, как вы структурируете особые экраны, которые очень уникальны. Следует иметь в виду, что создание такого приложения должно передаваться в библиотеки, такие как epoxy на Android, что дает пользователю возможность обрабатывать различные виды дочерних элементов в списке для создания эффективных собственных приложений.

Я надеюсь, что в будущем я смогу дать вам реальные примеры по этому поводу, я думаю, что мои следующие главы будут включать приложение на основе nodeJS для получения информации из разных источников и раскрытия ее с помощью GraphQL API, а также пример с использованием NextJS и Android / iOs Native. . Время покажет, и мы увидим.