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

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

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

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

Привязка данных (binding)

Шестнадцатый урок упаковал иконки и шрифты в ресурсы — интерфейс стал законченным, но данные по-прежнему таскались в виджеты руками: SetText в обработчике, Refresh после правки среза. Пакет fyne.io/fyne/v2/data/binding переворачивает схему: значение становится объектом-источником, который сам сообщает подписчикам об изменениях, а виджеты подписываются методом Bind. Запрос «fyne binding» — это binding.String и его братья Float, Bool, Int, DataItem как общий интерфейс и DataListener как механизм уведомлений; «привязка данных fyne» целиком стоит на этих трёх опорах. Урок ведёт от первого двустороннего Entry через слушателей и преобразования к правилу одной истины: данные живут в источнике, виджеты только отображают. Заодно закладывается фундамент следующего урока — валидация ввода начинается с честного вопроса о том, куда и как течёт значение из поля.

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

От ручной синхронизации к источникам данных#

Восемнадцать уроков подряд ты доставал данные в интерфейс по личному распоряжению: SetText в обработчике кнопки, RefreshItem после удаления записи, обновление метки после каждого клика. Пока источник один и обработчиков два, схема живёт; но когда значений становится десятки, а менять их нужно из разных мест — из меню четырнадцатого урока, из диалогов тринадцатого, из фоновых задач — ручная синхронизация начинает пропускать обновления, и интерфейс врёт. Пакет fyne.io/fyne/v2/data/binding решает задачу архитектурно. Запрос «fyne binding» — это источники данных, которые сами сообщают подписчикам об изменениях: значение перестаёт быть безликой переменной и становится объектом — DataItem с методами AddListener и RemoveListener. Над DataItem построены типизированные интерфейсы: binding.String с парой Get() (string, error) и Set(string) error, а рядом binding.Float, binding.Bool и binding.Int. Источник создаётся фабрикой: binding.NewString() даёт пустую строку, NewFloat(), NewBool() и NewInt() — нули; начального значения в конструкторе нет, заполнение делается через Set. Заметь контракт ошибок: и Get, и Set возвращают error. У фабричного источника она всегда nil — таково правило игры; но у источников, привязанных к внешнему миру, ошибка настоящая, и код честно получает пару (v, err), а не падает на месте. Третья опора — уведомление: после каждого Set источник рассылает слушателям сигнал DataChanged, и каждый подписчик перечитывает значение через Get. Виджеты уже умеют быть подписчиками: метод Bind есть у Entry, Label, Check, Slider, ProgressBar и Select. Разберём первый пример.

Первый источник: Entry и Label на одном значении

GO
package main

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

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

	note := binding.NewString()
	note.Set("Первая привязка")

	entry := widget.NewEntry()
	entry.Bind(note)

	mirror := widget.NewLabel("")
	mirror.Bind(note)

	window.SetContent(container.NewVBox(entry, mirror))
	window.ShowAndRun()
}

note — источник binding.String со значением, заданным до создания виджетов. Entry.Bind подключает поле двусторонне: печать пользователя вызывает Set, и источник уведомляет всех. Label.Bind подключает подпись в одну сторону: она перечитывает Get при каждом изменении, и текст зеркалит поле без единого обработчика OnChanged.

Пользователь печатает в Entry, привязанном через Bind к binding.String. Что происходит со значением источника?

Скаляры и виджеты: кто с чем дружит#

Семейство источников покрывает все базовые типы интерфейса, и у каждого виджета есть свой естественный напарник. Текстовые — Entry и MultiLineEntry, а также Label — работают с binding.String. widget.Check и widget.Select нуждаются в булевом и строковом значении: Check привязывается к binding.Bool, галочка включена или выключена; Select хранит выбранный вариант как binding.String. Числа — царство binding.Float: Slider и ProgressBar обоих направлений привязываются к нему, поэтому громкость из ползунка и её отражение в прогресс-баре — это один источник и два Bind. Для целых счётчиков есть binding.Int, хотя на практике Float часто удобнее из-за прогресс-баров. Метод Bind принимает источник подходящего типа, компилятор не даст привязать Bool к Slider — типы защищают на этапе сборки. Противоположность — Unbind(): отвязывает виджет от источника и сбрасывает подписку; он обязателен при переиспользовании шаблонных элементов, о чём будет отдельный разговор ближе к концу урока. В англоязычной документации весь этот подход зовут fyne data binding: данные — единственный источник истины, виджеты — только отображение. Отсюда практическое правило перехода: если в коде появилась связка «обработчик меняет переменную и вручную обновляет метку», это кандидат на источник плюс Bind. Пять лет назад такие связки были нормой — в этом курсе они жили до девятого урока включительно; сегодня замени их привязкой, и класс рассинхронизации исчезнет как вид. Осталось показать связку Check-Slider-ProgressBar в сборе.

Экран громкости: один Float на три виджета

GO
package main

import (
	"fmt"

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

func main() {
	myApp := app.New()
	window := myApp.NewWindow("Громкость")

	volume := binding.NewFloat()
	volume.Set(0.4)
	muted := binding.NewBool()

	slider := widget.NewSlider(0, 1)
	slider.Bind(volume)

	bar := widget.NewProgressBar()
	bar.Bind(volume)

	label := widget.NewLabel("")
	label.Bind(binding.FloatToString(func(f float64) string {
		return fmt.Sprintf("Громкость: %.0f%%", f*100)
	}))

	mute := widget.NewCheck("Без звука", nil)
	mute.Bind(muted)

	window.SetContent(container.NewVBox(slider, bar, label, mute))
	window.ShowAndRun()
}

Три виджета на одном источнике: пользователь двигает слайдер — Set уходит в volume, прогресс-бар и подпись обновляются подпиской. Подпись показывает преобразование: FloatToString оборачивает Float в String с функцией форматирования, и Label.Bind получает уже готовый текст. Check на отдельном источнике Bool — булево состояние не смешивается с числом.

Какой источник нужен widget.Check для привязки через Bind?

Два подписчика на одном источнике

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

name := binding.NewString()
name.Set("Том")

entry := widget.NewEntry()
entry.Bind(name)

label := widget.NewLabel("")
label.Bind(name)

// пользователь дописывает в поле: «Мария»
// что покажет label?

Слушатель подключается — и что происходит сразу

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

score := binding.NewFloat()
score.Set(3.5)

listener := binding.NewDataListener(func() {
        v, _ := score.Get()
        fmt.Println("счёт:", v)
})
score.AddListener(listener)

score.Set(4.0)
// сколько строк напечатается к этому моменту?

Слушатели: DataListener в чистом виде#

Механизм подписки доступен и без виджетов — иногда нужно реагировать на изменение значением, а не картинкой: писать лог, пересчитывать итоги, дёргать сетевой запрос. Для этого есть интерфейс DataListener с единственным методом DataChanged и удобный конструктор binding.NewDataListener(func()) — он заворачивает твой колбэк в готового слушателя. Подключение — AddListener на источнике, отключение — RemoveListener. Помни две детали. Первая: подписка немедленно синхронизируется — предсказание выше показывало, что колбэк срабатывает уже при AddListener, поэтому не полагайся в нём на то, что «изменения ещё не было». Вторая: снимай слушателя, когда он больше не нужен. Источник держит ссылку на каждого подписчика; слушатель, привязанный к источнику с долгой жизнью, переживёт свой экран и будет выполняться впустую — классическая утечка замыканий. Практическое правило: кто создал слушателя для экрана, тот и снимает его при закрытии; в простых приложениях с одним окном этим можно пренебречь, в многоэкранных — нет. Порядок доставки не гарантирован: источник обходит подписчиков в порядке подписки, но строить логику на строгой очередности — хрупкое решение; если один слушатель должен увидеть результат другого, объединяй их в один колбэк. И последнее: колбэк выполняется в той горутине, где случился Set. Об этом — жёлтая рамка ниже и двадцатый урок.

Слушатель как живой статус

GO
package main

import (
	"fmt"

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

func main() {
	myApp := app.New()
	window := myApp.NewWindow("Слушатель")

	temperature := binding.NewFloat()
	temperature.Set(20)

	status := widget.NewLabel("")

	listener := binding.NewDataListener(func() {
		v, err := temperature.Get()
		if err != nil {
			status.SetText("ошибка чтения")
			return
		}
		status.SetText(fmt.Sprintf("Температура: %.1f°", v))
	})
	temperature.AddListener(listener)

	up := widget.NewButton("+0.5", func() {
		v, _ := temperature.Get()
		temperature.Set(v + 0.5)
	})

	window.SetContent(container.NewVBox(status, up))
	window.ShowAndRun()
}

Слушатель читает значение через Get с честной проверкой err и пишет текст в метку — без Bind, чистой подпиской. Заметь: метка заполнится ещё до первого клика, потому что AddListener вызывает DataChanged сразу. Кнопка читает текущее значение и пишет обратно — источник остаётся единственным местом хранения.

Почему слушатель, ставший ненужным, стоит снимать через RemoveListener?

Внешние значения и преобразования#

Не всегда значение рождается внутри источника: иногда оно уже живёт в поле твоей структуры, и хочется привязать интерфейс к нему, не переписывая модель. Для этого есть семейство Bind: binding.BindString(&myField) возвращает ExternalString — источник, у которого Set пишет и в хранилище, и в твою переменную, а метод Reload() перечитывает внешнее значение и рассылает уведомления, если данные поменялись извне. Аналогично устроены BindFloat, BindBool и BindInt с указателями на float64, bool и int. Правило одно: переменная обязана жить достаточно долго — указатель на локальную переменную функции приведёт к висячей привязке. Второй инструмент — преобразования. binding.StringToString(func(s string) string) даёт источник-надстройку: он слушает исходный и выдаёт преобразованное значение; пример с громкостью показывал FloatToString для процентов. Если преобразование должно работать в обе стороны — из отображения в данные при правке поля — бери FloatToStringWithReverse и братьев: конструктор принимает две функции, прямую и обратную, и привязанный Entry пишет в данные уже разобранное значение. Классика — температура в обе стороны или форматирование дат. Третий инструмент — коллекции: binding.NewUntypedList() создаёт источник списка, Append и Remove меняют состав, а widget.List.Bind подключает виртуализированный список из десятого урока к данным — элементы списка получают свои мини-источники, и RefreshItem становится не нужен: изменение элемента обновляет только его строку. Для таблиц одиннадцатого урока есть табличные привязки — механика та же, детали — в документации пакета.

Каталог API привязки#

APIЧто делаетКогда звать
binding.NewString() / NewFloat() / NewBool() / NewInt()создать пустой источник-скалярзначение состояния рождается в приложении
item.Set(v) и v, err := item.Get()записать и прочитать значение; оба возвращают errorв обработчиках и слушателях
widget.Bind(item)привязать виджет к источнику; Entry — двусторонневместо ручных SetText и OnChanged
widget.Unbind()отвязать виджет и снять подпискупереиспользование шаблона элемента
binding.BindString(&ext)внешнее значение: Set пишет в переменную, Reload перечитываетданные живут в своей структуре
binding.NewDataListener(func())слушатель из обычного колбэкареакция на изменение без виджета
AddListener / RemoveListenerподписка и её снятие; подписка срабатывает сразуне забывай снимать лишних
binding.StringToString(f)надстройка-преобразование для отображенияформат вывода без правки данных
binding.FloatToStringWithReverse(f, g)преобразование в обе стороныполе правит данные через конвертер
binding.NewUntypedList()источник динамического спискаList.Bind вместо ручного RefreshItem
binding.BindPreferenceString(key, prefs)источник прямо из хранилища настроекмост к девятнадцатому уроку

Собери привязку со слушателем

Восстанови пять пропусков: создание источника, привязка поля, фабрика слушателя, чтение с ошибкой и подключение подписки.

name := binding.() name.Set("Гость") entry := widget.NewEntry() entry.(name) listener := binding.(func() { v, := name.Get() fmt.Println("имя:", v) }) name.(listener)

Путь значения от Set до экрана

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

Строки

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

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

Архитектура: одна истина#

Главный подарок привязки — не экономия на SetText, а дисциплина состояния. Одна истина: значение живёт в источнике, и виджет — лишь окно в него; два места хранения одного значения гарантированно разъедутся, вопрос только когда. Практика: собирай источники в структуру состояния приложения — App struct с полями-привязками создаётся при старте и передаётся экранам; обработчики меняют источники, экраны подписаны, и новая фича не требует ревизии всех старых обработчиков. Не дублируй: если текст поля уже в binding.String, не храни копию в переменной «на всякий случай» — читай Get в момент нужды. Следи за переиспользованием: в шаблоне элемента List из десятого урока виджет один, а значений много — перед привязкой нового значения зови Unbind, иначе подписка от прошлой строки продолжит писать в свежие данные, и список начнёт путать строки под прокруткой. Это самый частый баг новичка в коллекциях с привязкой. Разумно делить: не превращай всё подряд в источники — локальная переменная внутри обработчика не заслуживает DataItem; привязка окупается там, где значение видно более чем в одном месте или меняется более чем из одного места. Помни про ошибки: Get возвращает пару, и источники, привязанные к внешнему миру, вправе ответить не-nil — обрабатывай хотя бы до уровня «показать ShowError из тринадцатого урока». Наконец, мости уровни: доменная модель пусть остаётся обычными структурами, а источники — тонким слоем между моделью и экраном; BindString-семейство как раз для этого.

Что дальше: валидация ввода#

Теперь данные текут: поле пишет в источник, подписчики обновляются сами, слушатели логируют каждое изменение. Но поток бесполезен, если в нём мусор — «abc» в поле возраста доедет до источника так же честно, как «42». Опора урока — девятый с Entry и его событиями, десятый с шаблонами списка и Unbind, второй с каркасом запуска. Следующая тема — валидация ввода: встроенные валидаторы Entry, свои правила func(string) error, красная рамка и блокировка Submit, пока данные не стали съедобными; привязка и валидация работают в паре — проверенное значение заслуживает права попасть в источник. Домашнее задание соберёт панель счётчика на одном Float с тремя виджетами и слушателем-логгером. Полный маршрут — на странице курса Fyne.

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

Термин

binding.String и братья

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

Типизированные источники данных: Get() (T, error) и Set(T) error. Создаются NewString/NewFloat/NewBool/NewInt; после Set рассылают DataChanged подписчикам.

1 / 6

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

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

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

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

…

Журнал событий

Экран собран: «binding»

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

  • Пакет fyne.io/fyne/v2/data/binding делает значение объектом-источником: DataItem с подпиской, типизированные String, Float, Bool, Int с парой Get/Set и честным error.
  • widget.Bind подключает виджет к источнику: Entry и Slider пишут в данные сами, Label и ProgressBar только показывают; Unbind снимает привязку.
  • После каждого Set источник рассылает DataChanged: сколько бы подписчиков ни висело, все перечитают значение — ручной SetText больше не нужен.
  • AddListener срабатывает немедленно: слушатель и привязанный виджет видят текущее значение сразу; ненужных слушателей снимай RemoveListener.
  • BindString подключает внешнюю переменную, StringToString и WithReverse-семейство преобразуют значения для отображения и обратно.
  • NewUntypedList с List.Bind переводит коллекции на привязку — изменение элемента обновляет только его строку без RefreshItem.
  • Правило одной истины: данные в источниках, виджеты — отображение; Set из чужой горутины — только через fyne.Do, детали в двадцатом уроке.

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

Собери панель счётчика объёма на одном источнике: binding.NewFloat, Slider и ProgressBar привязаны к нему, Label показывает проценты через FloatToString, кнопка «Сброс» пишет Set(0). Добавь ручного слушателя через NewDataListener, который печатает каждое значение в терминал, — и проверь предсказание урока: строка со стартовым значением должна появиться ещё до первого клика. Сделай второй экран на том же Float: переключение вкладкой из двенадцатого урока, и убедись, что оба экрана показывают одно значение без единого обработчика синхронизации. Смоделируй список: NewUntypedList, кнопка добавления Append, List.Bind с шаблоном из десятого урока — и проверь, что удаление через Remove обновляет только нужную строку. Ловушка для самопроверки: попробуй переиспользовать один Entry в шаблоне без Unbind и добейся путаницы строк; найди минимальный сценарий, где она проявляется. Если захочешь писать в источник из горутины — остановись и перечитай жёлтую рамку: правильный инструмент появится в двадцатом уроке.

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

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

Чем binding лучше привычных OnChanged и SetText?

OnChanged ловит только правки пользователя, а источнику без разницы, кто вызвал Set: код, слушатель или другой виджет. Привязка убирает целый класс рассинхронов: не нужно помнить, какие метки обновлять после каждого изменения, — подписчики перечитывают значение сами.

Можно ли вызывать Set и Get из горутин?

Сам источник потокобезопасен, но колбэки слушателей выполняются в горутине вызова, и обновление виджетов из чужого потока запрещено каноном GUI. Пиши в источники через fyne.Do из двадцатого урока — тогда уведомления разойдутся по правилам.

Как привязать целую структуру, а не отдельное поле?

Готовых структурных источников нет: держи модель обычными структурами и подключай поля по одному через BindString-семейство, либо сериализуй структуру в строку и привязывай её. Для коллекций — NewUntypedList и табличные привязки из документации пакета.

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

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

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

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

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

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

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

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

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