Слайс UI - паттерн ui-slice и работа со списками в пользовательском интерфейсе

05 января 2026
Автор

Олег Марков

Введение

Термин «Слайс UI» или «ui-slice» обычно используют, когда говорят о том, как представить состояние пользовательского интерфейса в виде слайса структур и как работать с этим состоянием так, чтобы код оставался простым и предсказуемым.

Чаще всего это касается экранов со списками: таблицы, коллекции карточек, списки задач, чатов, новостей. Вы храните элементы списка как слайс, меняете этот слайс (добавляете, удаляете, сортируете), а UI «просто» должен отразить изменения.

На практике вокруг этого «просто» возникает много вопросов:

  • как хранить состояние строки/ячейки (selected, loading, error);
  • как синхронизировать слайс данных и видимые элементы;
  • как безопасно изменять слайс при обновлениях с сервера;
  • как не потерять пользовательские изменения (выбор, раскрытие, редактирование);
  • как сделать код читаемым и тестируемым.

В этой статье я покажу вам, как можно организовать слой «Слайс UI» (ui-slice) так, чтобы он был:

  • предсказуемым;
  • простым в отладке;
  • удобным для тестов;
  • независимым от конкретного фреймворка (Android/iOS/Web/desktop);
  • безопасным в многопоточном коде (если вы работаете на Go или другом языке с конкуррентностью).

Основная идея: вы описываете один «UI-элемент» как структуру, а UI-слой работает не с отдельными полями, а со слайсом этих элементов и заранее определенным набором операций над этим слайсом.


Что такое ui-slice и зачем он нужен

Слайс как источник правды для UI

Когда мы говорим «ui-slice», мы подразумеваем, что:

  • список элементов UI представлен в виде слайса;
  • каждое изменение UI отражается как изменение этого слайса;
  • сам слайс считается «источником правды» (single source of truth) для видимого списка.

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

// ItemUI описывает один элемент списка в UI
type ItemUI struct {
    ID       string // Уникальный идентификатор элемента
    Title    string // Текст заголовка
    Selected bool   // Флаг - выбран ли элемент в UI
    Loading  bool   // Флаг - находится ли элемент в состоянии загрузки
    Error    string // Текст ошибки, связанной с элементом (если есть)
}

// UISlice - это просто слайс элементов UI
type UISlice []ItemUI

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

UI-слой (виджеты, компоненты, экраны) не хранит в себе отдельные поля для каждого элемента. Он читает только UISlice и реагирует на его изменения.

Какие задачи решает ui-slice

Паттерн ui-slice помогает решить несколько типичных задач:

  • Синхронизация состояния
    Вы меняете только данные (слайс), UI автоматически обновляется. Не нужно вручную «поддерживать» одновременно и данные, и отдельные внутренние состояния виджетов.

  • Управление выбором и фокусом
    Логика выбора (selected, highlighted, expanded) описывается на уровне слайса, а не разбросана по разным компонентам.

  • Переиспользование и тестирование
    Все операции над ui-slice можно тестировать как обычные функции, без запуска всего UI.

  • Упрощение архитектуры
    Controller/Presenter/ViewModel возвращает всегда один тип – UISlice. UI не знает о доменной модели напрямую.


Базовое устройство ui-slice

Структура элемента UI

Давайте разберемся, что обычно входит в элемент ui-slice. Ниже пример чуть более насыщенной структуры:

// ItemUI описывает состояние одной строки списка в UI
type ItemUI struct {
    ID        string // Уникальный ID для стабильного сопоставления
    Title     string // Основной текст строки
    Subtitle  string // Дополнительная подпись
    IconURL   string // Ссылка на иконку (если есть)
    Selected  bool   // Отмечен ли элемент
    Disabled  bool   // Можно ли с ним взаимодействовать
    Loading   bool   // Идет ли по нему операция
    ErrorText string // Локальная ошибка, связанная с действием по элементу
}

Обратите внимание: здесь мы описываем именно UI-состояние, а не бизнес-домен. Скажем, в доменной модели задачи может не быть флагов Selected, Disabled, Loading, но для UI они нужны.

Частая практика:

  • делать отдельные типы для доменной модели (Task, User, Message);
  • конвертировать их в ItemUI с помощью функции маппинга;
  • хранить в ui-slice только ItemUI (или аналогичные структуры).

Тип слайса и базовые операции

Определяем тип ui-slice и функцию-конструктор:

// UISlice - тип для списка элементов UI
type UISlice []ItemUI

// NewUISlice создает UISlice из слайса доменных моделей
func NewUISlice(tasks []Task) UISlice {
    ui := make(UISlice, 0, len(tasks))
    for _, t := range tasks {
        ui = append(ui, mapTaskToItemUI(t)) // Маппинг доменной модели в UI-модель
    }
    return ui
}

Где:

// Task - пример доменной модели
type Task struct {
    ID    string
    Title string
    Done  bool
}

// mapTaskToItemUI преобразует Task в ItemUI
func mapTaskToItemUI(t Task) ItemUI {
    return ItemUI{
        ID:       t.ID,
        Title:    t.Title,
        Subtitle: "",          // Доп. поле, если нужно
        Selected: false,       // В домене нет, но в UI будет
        Disabled: t.Done,      // Например, выполненные задачи делаем disabled
    }
}

Теперь вы можете поддерживать ui-slice отдельно от доменной модели и четко видеть, какие поля относятся только к UI.


Операции над ui-slice

В реальном приложении ui-slice почти никогда не остается статичным. Вы будете:

  • добавлять элементы;
  • удалять элементы;
  • обновлять часть полей;
  • сбрасывать состояние (например, при рефреше данных);
  • менять порядок элементов.

Иммутабельный и мутабельный подход

Есть два распространенных подхода:

  1. Иммутабельный
    Каждая операция над слайсом возвращает новый ui-slice.
    Удобно в реактивных фреймворках и для предсказуемости.

  2. Мутабельный
    Операции изменяют слайс «на месте».
    Удобно в производительных и низкоуровневых частях (как в Go), но требует аккуратности.

Давайте реализуем оба подхода, чтобы вы увидели отличие.


Иммутабельные операции

Предположим, мы хотим всегда возвращать новый UISlice.

// WithToggledSelection возвращает новый UISlice
// с инвертированным флагом Selected у элемента с указанным ID
func (s UISlice) WithToggledSelection(id string) UISlice {
    // Копируем исходный слайс
    out := make(UISlice, len(s))
    copy(out, s)

    for i, item := range out {
        if item.ID == id {
            // Инвертируем флаг Selected
            out[i].Selected = !item.Selected
            break
        }
    }
    return out
}

Комментарий:

  • мы не меняем исходный слайс;
  • создаем новый слайс той же длины и копируем данные;
  • изменяем элемент только в новом слайсе.

Такой подход упрощает отладку: вы можете хранить «историю состояний» и откатываться назад (time travel debugging).

Добавление элемента:

// WithAppendedItem возвращает новый UISlice
// с добавленным элементом в конец
func (s UISlice) WithAppendedItem(item ItemUI) UISlice {
    out := make(UISlice, len(s), len(s)+1)
    copy(out, s)
    out = append(out, item)
    return out
}

Удаление элемента:

// WithRemovedByID возвращает новый UISlice без элемента с указанным ID
func (s UISlice) WithRemovedByID(id string) UISlice {
    out := make(UISlice, 0, len(s))
    for _, item := range s {
        if item.ID == id {
            // Пропускаем этот элемент
            continue
        }
        out = append(out, item)
    }
    return out
}

Вы можете комбинировать такие функции-операции в цепочки:

// Здесь мы показываем как можно последовательно применить несколько операций
slice := NewUISlice(tasks)
slice = slice.
    WithAppendedItem(newItem).
    WithToggledSelection(newItem.ID).
    WithRemovedByID(oldID)

Мутабельные операции

В некоторых случаях вам важна производительность и вы готовы работать с изменением «на месте». Тогда операции могут выглядеть так:

// ToggleSelectionInPlace изменяет флаг Selected у элемента прямо в исходном UISlice
func (s UISlice) ToggleSelectionInPlace(id string) {
    for i := range s {
        if s[i].ID == id {
            s[i].Selected = !s[i].Selected
            return
        }
    }
}

Удаление «на месте» обычно делают через стандартный прием со сдвигом:

// RemoveByIDInPlace удаляет элемент по ID, сдвигая хвост слайса
func (s *UISlice) RemoveByIDInPlace(id string) {
    a := *s
    for i := range a {
        if a[i].ID == id {
            // Здесь мы перемещаем элементы справа от i на одну позицию влево
            copy(a[i:], a[i+1:])
            // Укорачиваем слайс на 1
            *s = a[:len(a)-1]
            return
        }
    }
}

Обратите внимание:

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

Связь ui-slice с UI-фреймворком

Подход с «рендер-функцией»

Обычно архитектура строится по схеме:

  1. Храним текущее состояние UISlice где-то в модели/сторе.
  2. На каждое изменение ui-slice вызываем функцию рендеринга UI.

Условно:

type UIUpdater interface {
    RenderList(items UISlice)
}

Контроллер или ViewModel:

type ListController struct {
    ui      UIUpdater // Интерфейс для обновления UI
    uiSlice UISlice   // Текущее состояние списка в виде ui-slice
}

// SetItems полностью заменяет список
func (c *ListController) SetItems(tasks []Task) {
    // Обновляем uiSlice
    c.uiSlice = NewUISlice(tasks)

    // Передаем обновленный список в UI
    c.ui.RenderList(c.uiSlice)
}

// OnItemClicked вызывается, когда пользователь нажал на элемент
func (c *ListController) OnItemClicked(id string) {
    // Меняем состояние выбранности
    c.uiSlice = c.uiSlice.WithToggledSelection(id)

    // Обновляем UI
    c.ui.RenderList(c.uiSlice)
}

Как видите, UIUpdater скрывает детали конкретного фреймворка. Вы можете реализовать его по-разному для web, mobile, desktop.

Рекомендации по обновлению UI

Чтобы ui-slice действительно помогал, полезно придерживаться нескольких правил:

  1. UI обновляется только из одного места
    Все изменения UI-состояния идут через контроллер/стор, а не напрямую из разных мест.

  2. UI не меняет ui-slice напрямую
    Компоненты UI вызывают методы контроллера (OnItemClicked, OnRefresh, OnLoadMore), а он уже меняет ui-slice.

  3. Слайс – единый формат для списка
    Даже если у вас несколько типов строк (заголовок, обычная строка, футер), все равно используйте один тип слайса, а различайте элементы по полю Type.


Сложные элементы в ui-slice

Разные типы элементов в одном списке

Частая задача – в одном списке показывать:

  • обычные элементы;
  • разделители;
  • заголовки секций;
  • заглушки (empty state);
  • индикатор загрузки.

В ui-slice это удобно решается через поле Type.

type ItemType int

const (
    ItemTypeContent ItemType = iota // Обычная строка с контентом
    ItemTypeHeader                  // Заголовок секции
    ItemTypeDivider                 // Разделитель
    ItemTypeLoading                 // Элемент с индикатором загрузки
    ItemTypeEmptyState              // Заглушка "нет данных"
)

type ItemUI struct {
    ID        string
    Type      ItemType // Тип элемента
    Title     string
    Subtitle  string
    Selected  bool
    Disabled  bool
    Loading   bool
    ErrorText string
}

Дальше ваши операции над слайсом могут работать с разными типами:

// WithLoadingFooter добавляет или обновляет "хвост" загрузки
func (s UISlice) WithLoadingFooter(show bool) UISlice {
    // Сначала удаляем предыдущий хвост, если был
    out := make(UISlice, 0, len(s)+1)
    for _, item := range s {
        if item.Type == ItemTypeLoading {
            // Этот элемент заменим новым при необходимости
            continue
        }
        out = append(out, item)
    }

    if show {
        // Добавляем элемент, который UI отрисует как индикатор загрузки
        out = append(out, ItemUI{
            ID:    "loading-footer",
            Type:  ItemTypeLoading,
            Title: "Загрузка...",
        })
    }
    return out
}

Здесь я добавляю «служебный» элемент в ui-slice, а UI просто знает, что Type=ItemTypeLoading нужно отрисовать как спиннер.


Группировка и секции

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

// BuildSectionedSlice создает список задач, разбитых по статусу
func BuildSectionedSlice(tasks []Task) UISlice {
    var out UISlice

    pending := filterTasksByStatus(tasks, StatusPending)
    done := filterTasksByStatus(tasks, StatusDone)

    if len(pending) > 0 {
        out = append(out, ItemUI{
            ID:    "header-pending",
            Type:  ItemTypeHeader,
            Title: "В работе",
        })
        out = append(out, mapTasksToItems(pending)...)
    }

    if len(done) > 0 {
        out = append(out, ItemUI{
            ID:    "header-done",
            Type:  ItemTypeHeader,
            Title: "Завершенные",
        })
        out = append(out, mapTasksToItems(done)...)
    }

    if len(out) == 0 {
        out = append(out, ItemUI{
            ID:    "empty",
            Type:  ItemTypeEmptyState,
            Title: "Нет задач",
        })
    }

    return out
}

Такой подход позволяет легко:

  • перестраивать секции;
  • добавлять фильтры и сортировку;
  • тестировать генерацию списка без UI.

Обработка ошибок и состояний загрузки

Локальные ошибки по элементам

Бывают операции, привязанные к одному элементу: «удалить задачу», «отправить сообщение». Здесь ui-slice хорошо помогает отображать локальные состояния и ошибки.

// WithItemLoading устанавливает флаг Loading для одного элемента
func (s UISlice) WithItemLoading(id string, loading bool) UISlice {
    out := make(UISlice, len(s))
    copy(out, s)

    for i := range out {
        if out[i].ID == id {
            out[i].Loading = loading
            if loading {
                // Сбрасываем предыдущую ошибку при новой попытке
                out[i].ErrorText = ""
            }
            break
        }
    }
    return out
}

// WithItemError устанавливает текст ошибки для одного элемента
func (s UISlice) WithItemError(id string, errText string) UISlice {
    out := make(UISlice, len(s))
    copy(out, s)

    for i := range out {
        if out[i].ID == id {
            out[i].Loading = false
            out[i].ErrorText = errText
            break
        }
    }
    return out
}

Смотрите, я специально сбрасываю Loading при установке ошибки. Так UI легко понимает, что операция завершилась и можно показать ошибку.

Глобальные состояния списка

Кроме локальных ошибок, у списка может быть глобальное состояние:

  • loading (первичная загрузка);
  • error (ошибка загрузки всего списка);
  • empty (нет данных).

Обычно эти состояния выносятся в отдельную структуру:

// UIState описывает общее состояние экрана со списком
type UIState struct {
    Items       UISlice // Собственно ui-slice
    Loading     bool    // Идет ли глобальная загрузка
    ErrorText   string  // Текст глобальной ошибки (если есть)
    CanRetry    bool    // Можно ли повторить
    EmptyText   string  // Текст для пустого состояния
    Refreshing  bool    // Флаг "pull to refresh"
    HasMoreData bool    // Есть ли еще страницы для догрузки
}

Контроллер может поддерживать UIState:

type ListController struct {
    ui    UIUpdater
    state UIState
}

func (c *ListController) LoadInitial() {
    // Устанавливаем состояние загрузки
    c.state.Loading = true
    c.state.ErrorText = ""
    c.state.Items = nil

    c.updateUI()

    // Здесь запускаем асинхронную загрузку
    go c.loadFromServer()
}

func (c *ListController) loadFromServer() {
    tasks, err := fetchTasks()
    if err != nil {
        // В случае ошибки обновляем состояние
        c.state.Loading = false
        c.state.ErrorText = "Не удалось загрузить список"
        c.updateUI()
        return
    }

    // В случае успеха строим ui-slice
    c.state.Loading = false
    c.state.ErrorText = ""
    c.state.Items = NewUISlice(tasks)
    if len(tasks) == 0 {
        c.state.EmptyText = "Нет задач"
    }
    c.updateUI()
}

func (c *ListController) updateUI() {
    // UIUpdater может принимать сразу весь UIState,
    // а не только список элементов
    c.ui.RenderList(c.state)
}

Здесь я показываю общий подход: ui-slice отвечает за структуру списка, а UIState – за «рамку» вокруг него (загрузка, ошибки, пустое состояние).


Конкурентный доступ и безопасность

Почему слайс нужно защищать

Если вы пишете на Go, вы почти наверняка столкнетесь с конкурентными операциями:

  • одна горутина загружает данные с сервера и обновляет ui-slice;
  • другая горутина обрабатывает клики пользователя и тоже меняет ui-slice;
  • третья горутина рендерит UI на основании ui-slice.

Слайс в Go – это структура, которая содержит:

  • указатель на массив элементов;
  • длину;
  • емкость.

Если вы одновременно меняете его из нескольких горутин без синхронизации, вы легко получите data race.

Использование мьютекса

Самый прямой способ – защитить ui-slice мьютексом.

type SafeUIStore struct {
    mu   sync.RWMutex
    data UIState
}

// Get возвращает копию текущего UIState
func (s *SafeUIStore) Get() UIState {
    s.mu.RLock()
    defer s.mu.RUnlock()

    // Здесь важно скопировать данные, а не отдавать ссылку
    state := s.data

    // Копируем слайс, чтобы вызывающий не мог его изменить
    if state.Items != nil {
        itemsCopy := make(UISlice, len(state.Items))
        copy(itemsCopy, state.Items)
        state.Items = itemsCopy
    }

    return state
}

// Update применяет функцию-мутацию к состоянию
func (s *SafeUIStore) Update(fn func(state *UIState)) {
    s.mu.Lock()
    defer s.mu.Unlock()
    fn(&s.data)
}

Теперь любые изменения состояния проходят через Update:

// Пример использования хранилища
func (c *ListController) OnItemClicked(id string) {
    c.store.Update(func(state *UIState) {
        state.Items = state.Items.WithToggledSelection(id)
    })
    c.updateUIFromStore()
}

func (c *ListController) updateUIFromStore() {
    state := c.store.Get()
    c.ui.RenderList(state)
}

Здесь вы видите, что:

  • мы не отдаем наружу ссылку на внутренний слайс;
  • любые изменения проходят в критической секции.

Тестирование ui-slice

Почему ui-slice удобно тестировать

Вся логика работы со списком:

  • выбор,
  • фильтрация,
  • группировка,
  • отображение ошибок,

может быть описана чистыми функциями, которые:

  • принимают ui-slice или UIState;
  • возвращают новый ui-slice или UIState;
  • не привязаны к UI-фреймворку.

Это значит, что вы можете писать обычные unit-тесты.

Пример теста для операций над ui-slice

// Здесь мы показываем пример простого теста для WithToggledSelection

func TestUISlice_WithToggledSelection(t *testing.T) {
    // Исходное состояние
    s := UISlice{
        {ID: "1", Title: "A", Selected: false},
        {ID: "2", Title: "B", Selected: true},
    }

    // Действие
    got := s.WithToggledSelection("1")

    // Проверяем, что первый элемент стал выбранным
    if !got[0].Selected {
        t.Errorf("expected item 1 to be selected")
    }

    // Проверяем, что второй элемент остался выбранным
    if !got[1].Selected {
        t.Errorf("expected item 2 to remain selected")
    }

    // Проверяем, что исходный слайс не изменился
    if s[0].Selected {
        t.Errorf("expected original slice to remain unchanged")
    }
}

Тест для построения секций:

func TestBuildSectionedSlice_Empty(t *testing.T) {
    var tasks []Task

    got := BuildSectionedSlice(tasks)

    if len(got) != 1 {
        t.Fatalf("expected 1 item, got %d", len(got))
    }

    if got[0].Type != ItemTypeEmptyState {
        t.Fatalf("expected first item to be empty state, got %v", got[0].Type)
    }
}

Такие тесты быстро выполняются и хорошо документируют поведение ui-slice.


Практические рекомендации по проектированию ui-slice

1. Всегда используйте стабильный ID

ID должен быть:

  • уникальным для списка;
  • постоянным между обновлениями данных.

Это важно для:

  • сопоставления элементов между старым и новым состоянием;
  • корректной анимации (если ваш UI-фреймворк ее поддерживает);
  • правильной обработки событий (клик по элементу с ID X).

Не используйте индекс в слайсе как ID, если порядок элементов может меняться.

2. Четко разделяйте доменную модель и UI-модель

Доменные структуры (Task, ChatMessage, Product):

  • описывают данные, как они есть в бизнес-логике;
  • не содержат флагов выбора/состояний UI.

UI-модель (ItemUI):

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

Это упрощает:

  • рефакторинг UI;
  • изменение визуального представления без изменения домена;
  • написание тестов, в которых вы работаете только с ui-slice.

3. Операции над ui-slice делайте маленькими и специализированными

Вместо одной «универсальной» функции, которая делает все сразу, лучше иметь набор простых:

  • WithToggledSelection
  • WithItemLoading
  • WithItemError
  • WithLoadingFooter
  • WithRemovedByID
  • BuildSectionedSlice

Так их проще:

  • комбинировать;
  • понимать;
  • тестировать.

4. Будьте осторожны с мутацией «на месте»

Если вы выбираете мутабельный стиль:

  • не изменяйте слайс, который может использоваться еще где-то;
  • по возможности копируйте слайс перед изменением, если он передается между слоями;
  • защищайте общий слайс мьютексом при конкурентном доступе.

5. Прописывайте правила обновления UI

Договоритесь в команде:

  • откуда можно менять ui-slice (одна точка входа – контроллер/стор);
  • как UI узнает об изменениях (колбэки, каналы, события);
  • как обрабатываются состояния загрузки и ошибок.

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


Заключение

Паттерн «Слайс UI» (ui-slice) опирается на идею, что состояние списка в интерфейсе должно быть представлено как простой слайс структур, а все изменения списка – как предсказуемые операции над этим слайсом.

Вы видели, как:

  • описать элемент UI как структуру с полями для отображения и состояния;
  • собирать из доменных моделей ui-slice и использовать его как источник правды;
  • реализовать операции над ui-slice в иммутабельном и мутабельном стиле;
  • добавить в список сложные элементы – заголовки секций, разделители, загрузочные хвосты;
  • описать глобальное состояние экрана с помощью UIState;
  • защитить ui-slice при конкурентном доступе;
  • удобно тестировать логику работы со списком.

Такой подход делает код UI слоя:

  • прозрачным – вы всегда видите, как именно формируется список;
  • безопасным – меньше шансов получить «рассинхрон» между данными и отображением;
  • гибким – вы легко меняете представление списка, не трогая доменную модель;
  • тестируемым – почти вся логика ui-slice проверяется обычными unit-тестами.

Если вы строите приложения со сложными списками, ui-slice может стать базовым строительным блоком архитектуры UI и заметно упростить жизнь при развитии и поддержке проекта.


Частозадаваемые технические вопросы по теме

Вопрос 1. Как обновлять один элемент ui-slice, если обновление приходит с сервера только по его ID

Самый удобный способ – сделать функцию, которая заменяет один элемент по ID:

// UpdateItemByID возвращает новый UISlice, в котором элемент с нужным ID заменен
func (s UISlice) UpdateItemByID(id string, update func(item ItemUI) ItemUI) UISlice {
    out := make(UISlice, len(s))
    copy(out, s)
    for i, it := range out {
        if it.ID == id {
            // Здесь вы вызываете функцию, которая вернет обновленную копию элемента
            out[i] = update(it)
            break
        }
    }
    return out
}

Использование:

slice = slice.UpdateItemByID("42", func(item ItemUI) ItemUI {
    item.Title = "Обновленный заголовок"
    item.ErrorText = ""
    return item
})

Так вы не нарушаете инварианты и не трогаете остальные элементы.


Вопрос 2. Как хранить в ui-slice временные ID для еще не сохраненных на сервере элементов

Используйте два поля:

  • ID (строка от сервера или пустая строка);
  • LocalID (генерируется на клиенте, уникален в пределах устройства).

UI-слой всегда работает с LocalID, а когда сервер вернет настоящий ID, вы:

  1. находите элемент по LocalID;
  2. заполняете поле ID;
  3. при следующей синхронизации ищете по ID.

Так вы не потеряете соответствие между локальными и серверными объектами.


Вопрос 3. Как организовать пагинацию в ui-slice без сильного усложнения

Держите в UIState флаги:

  • HasMoreData – есть ли еще страницы;
  • LoadingMore – идет ли догрузка.

В самом ui-slice добавляйте в конец элемент типа ItemTypeLoading, когда LoadingMore=true. При получении новой страницы:

  1. удаляете хвост загрузки (WithLoadingFooter(false));
  2. аппендите новые элементы;
  3. снова добавляете хвост, если HasMoreData=true.

UI просто рисует хвост как «спиннер внизу списка».


Вопрос 4. Как избежать постоянного копирования больших слайсов при иммутабельном стиле

Если количество элементов велико, а обновления локальны, используйте стратегию:

  • не копировать весь слайс, а копировать только при изменении;
  • по возможности использовать «копирование по частям» – например, copy для префикса и суффикса, если меняется только одна позиция;
  • рассмотреть использование структур-персистентных коллекций (на других языках) или аккуратно использовать мутабельные операции с четкими границами владения слайсом.

Для Go баланс обычно такой: иммутабельный стиль на уровне бизнес-логики и ограниченная мутация внутри контроллера под мьютексом.


Вопрос 5. Как лучше хранить состояние раскрытия/сворачивания элементов (expanded) в ui-slice

Добавьте в ItemUI флаг Expanded и управляйте им через отдельную операцию:

func (s UISlice) WithToggledExpanded(id string) UISlice {
    out := make(UISlice, len(s))
    copy(out, s)
    for i := range out {
        if out[i].ID == id {
            out[i].Expanded = !out[i].Expanded
            break
        }
    }
    return out
}

UI-слой при рендеринге смотрит на Expanded и выбирает нужный вид (например, показывает дополнительный текст или вложенный список). Такой флаг живет полностью в ui-slice и не влияет на доменную модель.

Стрелочка влевоВертикальные слайсы vertical-slices - практическое руководство для разработчиковСлайс модели в Go - работа с model-sliceСтрелочка вправо

Все гайды по Fsd

Открыть базу знаний

Отправить комментарий