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

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

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

Урок 19 из 25 ~16 мин +50 XP

Хранение настроек (preferences)

Восемнадцатый урок поставил фильтр на вход данных, но до сих пор всё, что приложение знало о пользователе, умирало вместе с окном: источники семнадцатого урока жили в памяти, и перезапуск обнулял счётчики, темы и черновики. Запрос «fyne preferences» — это fyne.Preferences из myApp.Preferences(): простое хранилище ключ-значение с SetString, SetFloat, SetBool и SetInt, вариантами WithFallback для честных значений по умолчанию и записью на диск без единой строчки кода работы с файлами. Русский запрос «настройки fyne» ведёт сюда же. Урок собирает полный цикл: сохранить, прочитать при старте со значением по умолчанию, услышать изменение слушателем и подключить хранилище к привязкам — так выбор темы из пятнадцатого урока переживёт закрытие окна, а экран настроек из полей восемнадцатого станет постоянным.

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

Настройки переживают перезапуск#

Приложение, которое каждый раз начинает с нуля, злит тихо, но надёжно: пользователь второй раз выбирает ту же тему, второй раз вводит имя, второй раз двигает громкость. Причина проста — все значения жили в памяти: источники семнадцатого урока умирают с процессом, и на диск не попадает ничего. Fyne решает это встроенным хранилищем: у каждого приложения есть fyne.Preferences, доступный как myApp.Preferences(). Запрос «fyne preferences» — это интерфейс с четырьмя парами для базовых типов: SetString(key, value) и String(key), SetBool(key, v) и Bool(key), SetFloat(key, v) и Float(key), SetInt(key, v) и Int(key). Плюс RemoveValue(key), который стирает ключ — на следующий запуск его будто никогда и не было. Хранилище ключ-значение: ключ — строка, значение — один из четырёх типов; никаких списков и структур, сложное сериализуй сам — об этом в разделе про практику. Главная ценность в том, что запись и чтение выглядят как работа со словарём, а сохранение на диск Fyne делает сам: ни путей, ни файлов, ни форматов. Русский запрос «fyne настройки» приводит в то же место. Вторая ценность — предсказуемость чтения: пустое хранилище возвращает пустые значения без ошибок и паник, а для честных значений по умолчанию есть семейство WithFallback, которое сейчас покажем в коде. Осталось собрать первый пример: приветствие, которое помнит имя.

Имя, которое запомнили

GO
package main

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

func main() {
	myApp := app.NewWithID("io.gomaster.notepad")
	window := myApp.NewWindow("Блокнот")

	prefs := myApp.Preferences()
	name := prefs.StringWithFallback("user.name", "Гость")

	greeting := widget.NewLabel("С возвращением, " + name)

	field := widget.NewEntry()
	field.SetPlaceHolder("Как к вам обращаться?")
	field.OnSubmitted = func(s string) {
		prefs.SetString("user.name", s)
		greeting.SetText("Запомнил: " + s)
	}

	window.SetContent(container.NewVBox(greeting, field))
	window.ShowAndRun()
}

Приложение создаётся с идентификатором — app.NewWithID из каркаса второго урока; на нём держится вся персистентность. При старте имя читается с запасным значением: первый запуск покажет «Гость», а после SetString каждый следующий — сохранённое имя. Ни одного импорта файловых пакетов: хранение настроек — работа хранилища.

Что вернёт prefs.String("never.set"), если ключ никогда не записывали?

Четыре типа и WithFallback#

Семейство WithFallback — главный инструмент загрузки при старте: StringWithFallback(key, fallback) вернёт сохранённое значение, а если его нет — запасное. Аналогично устроены BoolWithFallback(key, def), FloatWithFallback(key, def) и IntWithFallback(key, def). Семантика строгая: запасное значение подставляется только при отсутствии ключа; записанный false или ноль вернутся как есть. Отсюда правило выбора: для значений, где пустота и ноль — легитимное состояние, обычных String и Bool достаточно; для значений, где нужна разумная стартовая точка — громкость 0.5, тема «тёмная», язык «ru» — бери WithFallback и не пиши if-ы. Ключи именуй как пути в конфиге: «user.name», «theme.dark», «audio.volume» — группировка по префиксу спасает, когда ключей становится пара десятков; строковые константы для ключей собери в одном месте и не повторяй литералы по коду. Четыре типа — целые, дробные, булевы и строки — покрывают настройки как класс, но не данные: список задач или структуру профиля в preferences не положишь. Сериализуй в JSON-строку и храни её одним ключом — приемлемо для небольших блобов; всё, что больше пары килобайт, — территория файлов и хранилища приложения, это двадцать первый урок. И помни объём: preferences — про десятки мелких значений, читаемых быстро, а не про базу данных.

Два запуска, один язык

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

prefs := myApp.Preferences()
lang := prefs.StringWithFallback("lang", "ru")
fmt.Println("язык:", lang)

prefs.SetString("lang", "en")
// приложение закрыли и запустили снова
// что напечатается при втором запуске?

Где живут настройки: StoragePath и идентификатор#

Настройки пишутся в JSON-файл в каталоге пользователя, и путь определяется идентификатором приложения. Поэтому каркас начинается с app.NewWithID("io.gomaster.notepad") — по convention идентификатор пишется в обратном доменном стиле, как пакет для сторов. Если создать приложение через app.New() без идентификатора, Fyne выдаст ему случайный на каждый запуск: значения будут работать в памяти, но файл настроек каждый раз окажется новым, и всё написанное утонет. Это самый частый вопрос новичков — «почему настройки не сохраняются» — и самый частый ответ: добавь идентификатор. Каталог зависит от платформы и берётся из правил системы: на Linux это внутри ~/.config, на macOS — в ~/Library/Application Support, на Windows — в %AppData%; точное место подскажет метод StoragePath() хранилища. Внутри лежит preferences.json с твоими ключами — можно открыть и посмотреть, что именно сохранено, полезно при отладке. Запись на диск не мгновенная: Fyne копит изменения и сбрасывает файл с небольшой задержкой, поэтому не рассчитывай прочитать файл в ту же миллисекунду после Set — работай через API хранилища. На мобильных платформах из двадцать второго урока идентификатор обязателен вдвойне: там он становится именем пакета в сторе, а настройки живут в изолированном каталоге приложения. Один идентификатор — один набор настроек: смена ID обнулит видимые значения, это удобный способ получить «чистый профиль» для теста.

Счётчик запусков в двух вариантах

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

// вариант A
myApp := app.New()
// вариант B
myApp := app.NewWithID("io.gomaster.notes")

prefs := myApp.Preferences()
n := prefs.IntWithFallback("launches", 0)
prefs.SetInt("launches", n+1)
fmt.Println("запуск номер:", n+1)

// оба варианта закрывали и открывали по три раза
// что покажет каждый на третьем запуске?

Что обязательно для того, чтобы настройки пережили перезапуск на десктопе?

Слушатели изменений: хранилище тоже уведомляет#

Хранилище умеет то же, что источники семнадцатого урока, — сообщать об изменениях. AddChangeListener(func()) вешает колбэк, который вызывается, когда значения меняются; RemoveChangeListener(listener) снимает подписку. Сигнатура обманчиво пустая — func() без аргументов: хранилище не говорит, какой именно ключ изменился, оно сообщает факт перемены. Что делать колбэку? Перечитать интересующие ключи и обновить интерфейс — дешёвые чтения из памяти, так что перечитать можно хоть всё. Такой слушатель — удобное место для глобальных реакций: тема сменилась — перекрась самописные примитивы из седьмого урока, ведь виджеты перекрасятся сами, а canvas-примитивы хранят цвет значением и требуют Refresh. Порядок и группировка не гарантированы: несколько быстрых Set могут прийти одним уведомлением или двумя — не строй логику на счёте вызовов, перечитывай состояние целиком. Правила жизни те же, что у DataListener: снимаешь ненужного, не создаёшь циклов — колбэк, который сам пишет в хранилище, обязан избегать бесконечной эхо-петли: изменяй условно или проверяй, что значение реально поменялось, прежде чем писать ответ. На живом примере видно, как одна кнопка меняет булев ключ, а слушатель перерисовывает подпись — интерфейс и данные снова не встретились напрямую.

Слушатель хранилища в деле

GO
package main

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

func main() {
	myApp := app.NewWithID("io.gomaster.settings")
	window := myApp.NewWindow("Тема")

	prefs := myApp.Preferences()

	mode := widget.NewLabel("")
	update := func() {
		if prefs.BoolWithFallback("theme.dark", false) {
			mode.SetText("Тема: тёмная")
		} else {
			mode.SetText("Тема: светлая")
		}
	}
	update()
	prefs.AddChangeListener(update)

	toggle := widget.NewButton("Переключить", func() {
		prefs.SetBool("theme.dark", !prefs.BoolWithFallback("theme.dark", false))
	})

	window.SetContent(container.NewVBox(mode, toggle))
	window.ShowAndRun()
}

Функция update перечитывает булев ключ и пишет подпись; её вызов при старте заполняет метку до всяких изменений, а AddChangeListener делает её живой на будущее. Кнопка меняет только значение в хранилище — интерфейс реагирует через слушателя. Тот же ключ прочитает и полноценная тема из пятнадцатого урока.

Какая сигнатура у колбэка в Preferences().AddChangeListener?

Мост к привязкам: BindPreferenceString#

Семнадцатый урок оставил мост недостроенным — пора достроить. binding.BindPreferenceString(key, prefs) создаёт источник, живущий прямо в хранилище: Get читает ключ preferences, Set пишет в него же. Дальше всё знакомо: Entry.Bind(source) — и поле из девятого урока двусторонне связано с хранилищем; печать пользователя сохраняет значение, а загрузка при старте происходит сама, ведь источник читает ключ уже при подключении. Аналогично работают BindPreferenceBool для Check, BindPreferenceFloat для Slider и BindPreferenceInt. Получается самый короткий путь к экрану настроек: один Bind заменяет и поле ввода с обработчиком, и чтение при старте, и запись при изменении. Когда что выбирать? Прямые String и WithFallback удобны, когда значение читается в нескольких местах и записывается по событию — приветствие из первого примера; привязка удобна, когда значение — собственность одного контрола: имя пользователя в поле, громкость в слайдере, тема в галочке. Осторожность одна: привязка пишет на каждую правку поля, а не по кнопке «Сохранить» — для настроек это обычно и нужно, но если требуется явное подтверждение, оставь обычный источник семнадцатого урока и пиши в preferences по OnSubmit формы из восемнадцатого. Мост между уроками работает в обе стороны: проверка гарантирует, что в хранилище не попадёт мусор, а хранилище делает привязки персистентными без единой новой строки кода.

Экран настроек целиком на привязках

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.NewWithID("io.gomaster.prefs")
	window := myApp.NewWindow("Настройки")

	prefs := myApp.Preferences()

	name := widget.NewEntry()
	name.SetPlaceHolder("Имя пользователя")
	name.Bind(binding.BindPreferenceString("user.name", prefs))

	dark := widget.NewCheck("Тёмная тема", nil)
	dark.Bind(binding.BindPreferenceBool("theme.dark", prefs))

	volume := widget.NewSlider(0, 1)
	volume.Bind(binding.BindPreferenceFloat("audio.volume", prefs))

	window.SetContent(container.NewVBox(name, dark, volume))
	window.ShowAndRun()
}

Три контрола, три привязки — и ни одного обработчика: поле пишет имя в user.name, галочка управляет theme.dark, слайдер двигает audio.volume, всё сохраняется само и загружается при старте, потому что источники читают ключи прямо из хранилища. Закрой и открой приложение — состояние вернётся.

Каталог API настроек#

APIЧто делаетКогда звать
myApp.Preferences()получить хранилище приложенияодин раз при старте, дальше держи ссылку
app.NewWithID(id)создать приложение с постоянным идентификаторомвсегда, если настройки должны жить
SetString / Stringзаписать и прочитать строкуимена, пути, сериализованные блобы
StringWithFallback(key, def)чтение с запасным значениемзагрузка при первом запуске
SetBool / Bool и SetFloat / Floatбулевы и дробные значениягалочки, слайдеры, громкость
SetInt / Intцелые значениясчётчики, размеры, лимиты
BoolWithFallback / FloatWithFallback / IntWithFallbackчтение с запасным значениемразумные стартовые точки
RemoveValue(key)стереть ключсброс отдельной настройки
AddChangeListener / RemoveChangeListenerподписка на факты измененияглобальные реакции: тема, заголовки
StoragePath()путь, где лежат сохранённые значенияотладка и бэкап вручную
binding.BindPreferenceString(key, prefs)источник привязки из хранилищаэкран настроек на Bind

Собери загрузку при старте

Восстанови пять пропусков: получение хранилища, привязка к нему поля и три чтения с запасными значениями.

prefs := myApp.() name := widget.NewEntry() name.Bind(binding.("user.name", prefs)) title := prefs.("doc.title", "Без названия") dark := prefs.("theme.dark", true) volume := prefs.("audio.volume", 0.5)

Путь настройки от слайдера до файла на диске

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

Строки

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

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

Практика состояния: что стоит сохранять#

Соберём короткий список того, что в реальных приложениях живёт в настройках. Выбор темы: галочка тёмного режима из примера выше читается при старте до построения интерфейса, и тема из пятнадцатого урока применяется сразу — обещание того урока исполнено; меняется — слушатель перекрашивает остатки. Черновики: текст редактора в String по таймеру или OnChanged — пользователь перезапустил приложение и нашёл текст на месте; для больших текстов — файл. Флаги «больше не показывать» и «первый запуск»: Bool-ключ превращает онбординг в одноразовый, а итоговый счётчик запусков удобно показать в ShowInformation из тринадцатого урока. Счётчики: число запусков из предсказания, номер последней открытой вкладки, геометрия окна — размеры и позицию Fyne сам не помнит, два Int-ключа решают дело. Что НЕ стоит: всё, что изменяется часто и требует истории — журнал действий; всё, что должно жить на сервере и путешествовать между машинами — preferences локальны по определению; всё, что пользователь не выбирал сам, — кеш не настройка. Проверь связку при старте: читать настройки лучше до ShowAndRun из второго урока, чтобы первый кадр уже отражал сохранённое состояние, — мигание «дефолт, потом тёмная тема» выглядит как баг, даже если это просто порядок инициализации.

Что дальше: потоки и fyne.Do#

Теперь приложение помнит пользователя: значения переживают перезапуск, тема возвращается, экран настроек собран из трёх привязок без обработчиков. Опора урока — второй с каркасом запуска и идентификатором, семнадцатый с источниками и Bind, восемнадцатый с проверкой значений на входе, пятнадцатый с темой, выбор которой теперь сохраняется. Следующая тема — многопоточность в GUI: что случится, если Set из семнадцатого урока позвать из горутины, почему интерфейс трогать можно только из главного потока и как fyne.Do делает bridges безопасными. Домашнее задание соберёт экран настроек, который переживает перезапуск целиком. Полный маршрут — на странице курса Fyne.

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

Термин

fyne.Preferences

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

Хранилище ключ-значение приложения из myApp.Preferences(): строки, булевы, дробные и целые значения с записью на диск без ручного кода файлов.

1 / 6

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

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

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

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

Настройки приложения

myApp.Preferences()

Строка, bool, float — по ключу, как в ini-файле

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

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

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

  • myApp.Preferences() — хранилище ключ-значение: SetString, SetBool, SetFloat, SetInt и чтение без ошибок: отсутствующий ключ даёт пустое значение.
  • WithFallback-семейство решает первый запуск: запасное значение возвращается только при отсутствии ключа, записанные ноль и false — как есть.
  • Персистентность держится на идентификаторе: app.NewWithID задаёт постоянный путь настройкам, app.New() выдаёт случайный ID и теряет всё.
  • Значения лежат в preferences.json в системном каталоге пользователя; точное место показывает StoragePath(), запись на диск — пакетная, не мгновенная.
  • AddChangeListener(func()) сообщает о переменах без имени ключа — перечитывай состояние; снимай слушателя и не строй эхо-петель.
  • BindPreferenceString/Bool/Float подключают хранилище напрямую к виджетам: экран настроек собирается без единого обработчика.
  • Храним выбор темы, черновики, флаги онбординга и счётчики; не храним секреты, кеши и большие данные — им место в файлах двадцать первого урока.

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

Собери экран настроек, который переживает перезапуск целиком: app.NewWithID, имя пользователя через BindPreferenceString и Entry, тёмная тема через BindPreferenceBool и Check, громкость через BindPreferenceFloat и Slider. Добавь подпись с процентом громкости на слушателе AddChangeListener и заголовок окна, который берёт имя из настроек при старте. Реализуй счётчик запусков на IntWithFallback и покажи его в ShowInformation из тринадцатого урока при третьем и далее запуске. Смоделируй выбор темы: флаг из настроек читается до ShowAndRun, и первое, что видит пользователь, — уже выбранная тема из пятнадцатого урока. Проверь обе стороны медали: закрой и открой приложение трижды — значения растут; затем запусти копию без идентификатора и убедись, что счётчик застрял на единице. Ловушка для самопроверки: попробуй записать в настройку большой JSON со списком задач и оцени, что произойдёт при частых правках; сделай вывод, где настоящая граница между настройками и данными, и запомни её для двадцать первого урока.

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

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

Почему настройки не сохраняются между запусками?

Почти всегда приложение создано через app.New() без идентификатора: Fyne выдаёт случайный ID на каждый запуск, файл настроек каждый раз новый. Создавай приложение через app.NewWithID с постоянной строкой в обратном доменном стиле — и значения начнут жить.

Можно ли хранить в preferences структуру или список?

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

Узнает ли слушатель, какой именно ключ изменился?

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

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

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

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

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

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

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

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

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

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