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

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 10 из 25 ~22 мин +50 XP

Списки и коллекции

Десять записей можно показать VBox-ом, сто — терпеть, а тысяча убьёт и память, и плавность прокрутки. Запрос «fyne list» — это widget.NewList: список с виртуализацией, в котором существует только то, что видно на экране. Три колбэка — length, createItem, updateItem — описывают данные, шаблон и заполнение, а пул элементов делает перечни любой длины дешёвыми. Списки fyne — фундамент почтовых клиентов, файловых браузеров и менеджера задач из итогового проекта: сегодня разберём контракт по косточкам и научимся не наступать на главную граблю виртуализации — состояние, которое переезжает между строками.

Содержание урока · 9 разделов

Каждый видел интерфейс, который тормозит от прокрутки: тысяча строк — тысяча виджетов, и каждый живёт в памяти, даже когда давно уехал за экран. widget.List решает это радикально: в дереве интерфейса существуют только видимые строки и пара про запас. Ты описываешь не тысячу виджетов, а три функции: length() сообщает, сколько всего элементов; createItem() создаёт шаблон одной строки; updateItem(id, item) наполняет шаблон данными конкретного элемента. Всё вместе — один вызов widget.NewList. Запрос «fyne list» ведёт именно сюда, и экономика такая: прокрутили список — тулкит не создаёт новые виджеты, а переиспользует освободившиеся из пула, меняя лишь их наполнение. Отсюда и главное правило урока, к которому мы придём через два примера: updateItem обязан полностью сбрасывать состояние строки, а не только включать нужное. Сначала — минимальный список, чтобы контракт стал привычным.

Минимальный список городов

GO
package main

import (
        "fyne.io/fyne/v2"
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/container"
        "fyne.io/fyne/v2/widget"
)

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Города")

        cities := []string{"Москва", "Казань", "Сочи", "Тверь", "Пермь"}
        details := widget.NewLabel("Выбери город")

        list := widget.NewList(
                func() int { return len(cities) },
                func() fyne.CanvasObject {
                        return widget.NewLabel("шаблон")
                },
                func(id widget.ListItemID, item fyne.CanvasObject) {
                        item.(*widget.Label).SetText(cities[id])
                },
        )
        list.OnSelected = func(id widget.ListItemID) {
                details.SetText("Выбрано: " + cities[id])
        }
        list.OnUnselected = func(id widget.ListItemID) {
                details.SetText("Выбор снят")
        }

        window.SetContent(container.NewHSplit(list, details))
        window.ShowAndRun()
}

Три колбэка NewList в действии: length возвращает len(cities), createItem отдаёт шаблон — новый Label, updateItem пишет в шаблон данные строки id, приводя объект к *widget.Label. OnSelected и OnUnselected ловят клики по строкам; выделение хранит сам список — повторный клик по выбранной строке снимает выбор и вызывает OnUnselected. HSplit делит окно: слева список, справа детали выбранного элемента.

Что возвращает первый колбэк NewList — func() int?

Пул элементов: кто на самом деле живёт в списке#

Виртуализация работает так. При отрисовке список спрашивает length(), вычисляет диапазон видимых строк для текущей прокрутки и для каждой достаёт объект из пула; пул пуст — зовётся createItem. Прокрутка уводит строку за экран — объект не умирает, а возвращается в пул; новая строка с другого конца — объект достаётся из пула и прогоняется через updateItem уже с новым id. Поэтому createItem вызывается считанные разы — примерно по числу видимых строк, — а updateItem крутится на каждом кадре прокрутки. Из этого следует всё остальное. Шаблон из createItem один и тот же и для строки «Москва», и для строки «Сочи»: любое состояние, которое ты не сбросил в updateItem, переедет от одного элемента к другому. Считай updateItem функцией, которая получает чужой объект и обязана привести его к полному соответствию данным id: текст, цвет, галочка, шрифт, важность — каждый атрибут задаётся в обеих ветках, если и да, и нет. Это единственная цена виртуализации — и её платят один раз, но осознанно.

Сколько раз зовётся createItem?

Прочитайте код и выберите, что он выведет:

// список на 10 000 строк, на экране видно 20
list := widget.NewList(
        func() int { return 10000 },
        func() fyne.CanvasObject {
                counter++
                return widget.NewLabel("шаблон")
        },
        func(id widget.ListItemID, item fyne.CanvasObject) {
                // наполнение
        },
)
// сколько раз вызовется createItem при открытии окна?

Состояние строки и обновление данных#

Классический баг виртуальных списков выглядит так: в updateItem строка с выполненной задачей красится зелёным, а для невыполненной ветка иначе просто не написана. Прокрутка — и зелёная строка прилипает к другим задачам: объект из пула пришёл с чужим цветом, а новый хозяин его не перекрасил. Лечение — всегда писать обе ветки: устанавливать и сбрасывать. Вторая тема — данные. Список не хранит элементы: он читает твой срез через length и updateItem, поэтому обновление выглядит всегда одинаково — меняешь срез, зовёшь list.Refresh(), и все видимые строки перечитываются. Точечная версия — list.RefreshItem(id): перечитать одну строку после правки, не трогая остальных; это дёшево и не сбивает прокрутку. Важно понимать природу id: это индекс в твоих данных, а не постоянный паспорт строки. После вставки в середину среза все id после точки вставки сдвинутся — и это нормально: при Refresh строки честно перечитаются и покажут актуальные данные. Хочешь устойчивость к перестановкам — храни в самих данных ключ и ищи по нему.

Задачи с чекбоксами и полный сброс

GO
package main

import (
        "fyne.io/fyne/v2"
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/container"
        "fyne.io/fyne/v2/widget"
)

type task struct {
        Title string
        Done  bool
}

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Задачи")

        tasks := []task{
                {Title: "Купить молоко", Done: true},
                {Title: "Написать отчёт", Done: false},
                {Title: "Позвонить маме", Done: false},
                {Title: "Прочитать главу", Done: true},
        }

        list := widget.NewList(
                func() int { return len(tasks) },
                func() fyne.CanvasObject {
                        return container.NewHBox(
                                widget.NewCheck("", nil),
                                widget.NewLabel("Шаблон задачи"),
                        )
                },
                func(id widget.ListItemID, item fyne.CanvasObject) {
                        row := item.(*fyne.Container)
                        check := row.Objects[0].(*widget.Check)
                        label := row.Objects[1].(*widget.Label)
                        t := tasks[id]

                        check.Checked = t.Done // напрямую: без вызова OnChanged
                        check.Refresh()
                        check.OnChanged = func(on bool) {
                                tasks[id].Done = on
                        }

                        label.SetText(t.Title)
                        if t.Done {
                                label.TextStyle = fyne.TextStyle{Strikethrough: true}
                        } else {
                                label.TextStyle = fyne.TextStyle{} // сброс — обязателен!
                        }
                        label.Refresh()
                },
        )

        window.SetContent(container.NewScroll(list))
        window.ShowAndRun()
}

Шаблон строки — HBox из Check и Label. updateItem делает полный сброс: check.Checked присваивается напрямую — вызов SetChecked дёрнул бы OnChanged и зациклил обработчик, — а стиль label задаётся в обеих ветках: зачёркнутый для выполненных и пустой для остальных. OnChanged навешивается после установки галочки и замыкает текущий id — клик правит срез. Убери пустую ветку TextStyle — и зачёркивание начнёт переезжать между строками на прокрутке: лучший способ прочувствовать пул.

После прокрутки зелёная подсветка «выполнено» показывается и на невыполненных задачах. Почему?

Управление списком: выделение и прокрутка#

У List есть и «руль» для программного управления. Select(id) выделяет строку — с теми же последствиями, что и клик: сработает OnSelected; UnselectAll() снимает выделение со всех строк. Колбэки OnSelected и OnUnselected получают ListItemID — по нему ты достаёшь данные и заполняешь панель деталей; повторный клик по выделенной строке снимает выбор. Прокрутка программируется тройкой: ScrollTo(id) открывает список на конкретной записи — типичный кейс после фильтрации или поиска, ScrollToTop() и ScrollToBottom() уводят к краям. RefreshItem(id) перечитывает одну строку, полный Refresh — весь список целиком. Поле HideSeparators прячет линии между строками — когда строки и так различимы, например обрамлены карточками. И ещё важное: список — обычный CanvasObject, он кладётся в любую компоновку четвёртого урока: в Border как центр, в HSplit рядом с деталями, в остаток VBox. Единственный запрет — не оборачивать список в container.NewScroll: прокрутка у него своя, и две прокрутки будут спорить за колесо мыши.

Живой поиск по списку

GO
package main

import (
        "fmt"
        "strings"

        "fyne.io/fyne/v2"
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/container"
        "fyne.io/fyne/v2/widget"
)

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Поиск по контактам")

        all := []string{
                "Анна Сергеева", "Борис Ким", "Вера Литвинова",
                "Глеб Морозов", "Дарья Осипова", "Егор Панин",
        }
        visible := all

        search := widget.NewEntry()
        search.PlaceHolder = "Начни вводить имя..."
        status := widget.NewLabel(fmt.Sprintf("Найдено: %d", len(all)))

        list := widget.NewList(
                func() int { return len(visible) },
                func() fyne.CanvasObject { return widget.NewLabel("шаблон") },
                func(id widget.ListItemID, item fyne.CanvasObject) {
                        item.(*widget.Label).SetText(visible[id])
                },
        )

        search.OnChanged = func(q string) {
                visible = nil
                for _, name := range all {
                        if strings.Contains(strings.ToLower(name), strings.ToLower(q)) {
                                visible = append(visible, name)
                        }
                }
                status.SetText(fmt.Sprintf("Найдено: %d", len(visible)))
                list.ScrollToTop()
                list.Refresh()
        }

        reset := widget.NewButton("Сбросить фильтр", func() {
                search.SetText("")
        })

        window.SetContent(container.NewBorder(
                container.NewVBox(search, reset),
                status, nil, nil, list,
        ))
        window.ShowAndRun()
}

Живой поиск из девятого урока на новых рельсах: OnChanged у Entry пересобирает срез visible, обновляет статус, зовёт ScrollToTop и list.Refresh() — length вернёт новую длину, updateItem перечитает видимые строки. SetText в кнопке сброса тоже дёргает OnChanged — фильтр снимается сам, без дублирования кода. Обрати внимание: список ни разу не пересоздан — меняются только данные и одна команда Refresh.

Что делает list.ScrollTo(42)?

Собери список задач

Восстанови четыре пропуска: длина данных, шаблон строки, тип идентификатора и прокрутка к строке.

list := widget.NewList( func() int { return (tasks) }, func() fyne.CanvasObject { return widget.("шаблон") }, func(id widget., item fyne.CanvasObject) { item.(*widget.Label).SetText(tasks[id].Title) }, ) list.(5) // прокрутить к шестой задаче

Жизнь одного элемента на экране

Расставь по порядку, что происходит с элементом списка при отрисовке и прокрутке.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Каталог API списков#

APIЧто делаетКогда звать
widget.NewList(length, createItem, updateItem)создаёт виртуальный список из трёх колбэководин раз при сборке интерфейса
length func() intобщее число элементов данныхсписок спрашивает при каждой перерисовке
createItem func() fyne.CanvasObjectшаблон одной строкитолько когда пул опустел
updateItem(id, item)наполнение шаблона данными строки idна каждую видимую строку и прокрутку
OnSelected / OnUnselectedреакция на выбор и снятие выбораклик по строке
Select / UnselectAllвыделить программно / снять выделениепо логике приложения
ScrollTo / ScrollToTop / ScrollToBottomпрокрутка к строке или краюпосле фильтрации, открытие на записи
Refresh / RefreshItem(id)перечитать все или одну строкупосле изменения данных

Производительность: updateItem под лупой#

Список прощает почти всё, кроме тяжёлого updateItem: он выполняется на каждый кадр прокрутки. Шаблон делай минимальным — Label дешевле RichText, а RichText дешевле панели из десятка объектов; иконки масштабируй заранее до нужного размера. Разбор данных — поиск подстроки, форматирование дат, сортировку — веди вне updateItem: срез уже подготовленных строк это правильный источник для length и updateItem, а не сырая база. Вложенный List внутри строки другого List работает, но это красный флаг архитектуры: почти всегда нужен Table из одиннадцатого урока или перегруппировка данных в плоский срез. Большие списки не любят частого полного Refresh: меняй точечно через RefreshItem, а массовые правки батчами с одним Refresh в конце. И помни про идентичность: не пытайся хранить соответствие «строка — объект», объектов у тебя нет — есть данные и id. Именно так устроен финальный менеджер задач из двадцать пятого урока: список задач, панель деталей и точечные обновления вместо перерисовки всего подряд.

Что дальше: таблицы, деревья и большой проект#

Компоновку под список давал четвёртый урок, Entry с OnChanged для живого поиска — девятый, а каталог виджетов с Check и Label — третий; события кликов по строкам продолжают шестой урок. Когда строки становятся двумерными — колонки, заголовки, дерево — наступает территория Table и Tree из одиннадцатого урока, он продолжает тему коллекций. Полный маршрут из двадцати пяти уроков — на странице курса Fyne. Домашнее задание соберёт адресную книгу с поиском, избранным и точечным обновлением — готовый скелет для почтового клиента или, после семнадцатого урока с binding, живого справочника.

Термины урока

Термин

widget.NewList

нажмите, чтобы перевернуть
Определение

Три колбэка: length func() int, createItem func() fyne.CanvasObject, updateItem func(id ListItemID, item fyne.CanvasObject). Виртуализация: в дереве живёт только видимое.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Виртуальный экран

HTML-модель виджет-дерева fyne
Спискиfyne · мок

10 задач — 1 виджет List

NewList — вне эмуляции
  • вне эмуляции: widget.NewList
Журнал событий

Экран собран: «Списки»

вне эмуляции: widget.NewList

Коротко: что запомнить

  • widget.NewList описывает список тремя колбэками: length, createItem, updateItem — данные, шаблон и наполнение.
  • Виртуализация: в дереве живут только видимые строки; createItem зовётся редко, updateItem — на каждый кадр прокрутки.
  • Главное правило пула: updateItem сбрасывает всё состояние строки в обеих ветках — иначе чужие цвета переезжают по списку.
  • Данные живут в твоём срезе: изменение плюс list.Refresh() или точечный RefreshItem(id).
  • Выделение — Select и UnselectAll с колбэками OnSelected и OnUnselected; прокрутка — ScrollTo, ScrollToTop, ScrollToBottom.
  • updateItem должен быть лёгким: подготовку данных веди в срезе, тяжёлое кешируй, полный Refresh заменяй точечным.
  • Список кладётся в любую компоновку, но не в Scroll: у него своя прокрутка; двумерные данные — территория Table.

Домашнее задание

Собери адресную книгу: срез из тридцати структур с именем и телефоном, строка списка — HBox из двух Label, OnSelected выводит карточку контакта в правую панель через HSplit. Добавь поиск через Entry и OnChanged: фильтруй срез, обновляй счётчик найденных, вызывай ScrollToTop и list.Refresh, кнопка «Сбросить» очищает поле через SetText. Затем добавь флажок избранного в строку: Check с прямым присвоением Checked и OnChanged, замыкающим текущий id; проверь на прокрутке, что флажки не переезжают между людьми — если переезжают, ты забыл сброс в ветке иначе. В конце добавь кнопку «К последнему контакту» со ScrollToBottom и убедись, что изменение телефона первого контакта можно применить точечно через RefreshItem, не трогая остальные строки. Ловушка для самопроверки: намеренно убери пустую ветку стиля и прокрути список — узнай баг пула вживую, а потом почини.

Личные заметкиПусто
Хранятся только в вашем браузере (Fyne · Списки и коллекции)

Вопросы и ответы по уроку

Сколько строк выдержит List?

Десятки тысяч без напряжения: в памяти существуют только видимые строки и пул про запас. Узкое место — updateItem: держи его лёгким, а данные готовь в срезе заранее, вне шаблона строки.

Почему при обновлении слетает выделение?

Список хранит выделение по id, а id — это индекс в данных: после пересортировки или вставки в середину среза прежний id указывает на другой элемент. Обновляй строки точечно через RefreshItem, а при перестройке данных восстанавливай Select по ключу из своих структур.

List внутри container.NewScroll — можно?

Нет: у списка своя прокрутка, и вложенные прокрутки спорят за колесо мыши. Списку просто отдают место компоновкой — центр Border, половина HSplit, остаток VBox; прокрутка содержимого строки решается внутри шаблона, а не снаружи.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Cobra25 уроков~10 чпрактика25 уроков уже доступны

Cobra: CLI-приложения на Go

Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.

  • Создавать CLI-приложения с деревом команд на Cobra
  • Работать с аргументами, флагами и их привязкой к конфигу
  • Управлять конфигурацией через Viper: файлы, env, приоритеты
  • Писать хуки PreRun/PostRun и корректно обрабатывать ошибки