LeCodesdocs

Жизненный цикл приложения, буфер обмена и openURL

Собственный жизненный цикл приложения (передний план/фон), URL, с которым его открыли (диплинки), системный буфер обмена и открытие внешних URL. app и clipboard — обычные глобальные объекты, ничего создавать не нужно.

Обзор

TypeScript
app.addEventListener('pause', () => saveGame())      // пользователь ушёл — сохраняйся сейчас
app.addEventListener('resume', () => refreshFeed())

if (app.launchUrl) openFromLink(app.launchUrl)       // диплинк холодного старта
app.addEventListener('url', url => openFromLink(url)) // …и ссылки, приходящие во время работы

clipboard.write('LECODES-42')
openURL('https://le.codes/docs')

Жизненный цикл: app.state, pause / resume

TypeScript
app.state                                   // "active" | "background"  (только чтение)
app.addEventListener('pause', () => { })    // приложение ушло с переднего плана
app.addEventListener('resume', () => { })   // приложение вернулось
app.removeEventListener('pause', callback)  // та же ссылка на функцию

"pause" срабатывает, когда приложение перестаёт быть приложением переднего плана — кнопка «домой», другое приложение сверху, скрытая вкладка браузера. Оно доставляется до того, как хост остановит цикл кадров, так что это последний надёжный момент сохранить состояние, поставить музыку на паузу или остановить работу, которой нельзя идти вслепую (watch у Geolocation, таймеры опроса). "resume" срабатывает, когда кадры снова идут.

События семантические и одинаковые на каждой платформе — словарь Activity/Scene ты не видишь никогда. app.state — та же информация в виде pull, читается в любой момент.

Note

не рассчитывай, что код работает, пока приложение в фоне — на мобильных цикл кадров останавливается и таймеры замирают до resume. pause — для сохранения, а не для планирования фоновой работы.

Диплинки: app.launchUrl и событие url

TypeScript
app.launchUrl                               // string | null  (только чтение)
app.addEventListener('url', (url: string) => { })

launchUrl — URL, с которым приложение было открыто (в последний раз) — null при обычном запуске. Когда ссылка приходит, пока приложение уже работает (тёплая ссылка), launchUrl обновляется на новое значение и с ним срабатывает событие "url". Обрабатывай оба случая, именно в этом порядке:

TypeScript
const openFromLink = (url: string) => {
  const screen = new URL(url).searchParams.get('screen')
  if (screen === 'stats') router.push(StatsScreen())
}
if (app.launchUrl) openFromLink(app.launchUrl)
app.addEventListener('url', openFromLink)
Note

гейтится хостом — launchUrl читается как null, а "url" никогда не срабатывает на хостах без понятия ссылки (headless).

Управление миром: app.restart / app.quit

TypeScript
app.restart(url?)     // перезапустить бандл приложения в свежем мире — location.reload()
app.quit()            // уйти в лаунчер LeCodes (домой) — в остальных случаях no-op

restart() перезапускает приложение с нуля: свежий мир, тот же код. Передай url, чтобы перезапуститься с другими аргументами запуска (новый запуск читает их через app.launchUrl), передай null, чтобы перезапуститься с очищенным, или опусти его, чтобы повторить текущий — аргумент зеркалит роль launchUrl как «командной строки» приложения. Несохранённое состояние пропадает, ровно как при перезагрузке страницы.

quit() возвращает пользователя в лаунчер LeCodes. В standalone-сборке лаунчера нет, поэтому он ничего не делает (платформы запрещают программный выход) — безопасно вешать на кнопку «назад в LeCodes» безусловно.

TypeScript
UIButton(UIText('Start over')).onClick(() => app.restart(null))
UIButton(UIText('Exit')).onClick(() => app.quit())

Клавиатура: app.keyboardHeight и событие keyboard {#keyboard}

TypeScript
app.keyboardHeight                          // number  (только чтение, логические px; 0, когда скрыта)
app.addEventListener('keyboard', (height: number, duration: number) => { })

keyboardHeightсырое перекрытие экранной клавиатуры с вьюпортом приложения, независимо от политики keyboardShrink сфокусированного инпута. С политикой сжатия по умолчанию layout и так избегает клавиатуры, так что оно нужно редко; главный потребитель — режим оверлея (keyboardShrink: false), где приложение само управляет своим инсетом. Событие срабатывает на каждое изменение фрейма клавиатуры; duration — длительность анимации клавиатуры на платформе в мс (0, где её нет) — передай её в animateTo, чтобы двигать UI синхронно:

TypeScript
// композер чата: клавиатура перекрывает UI, композер сам сдвигается вверх
input.style({ keyboardShrink: false })
app.addEventListener('keyboard', (height, duration) => {
  composer.animateTo({ transform: `translateY(${-height}px)`, duration })
})
Note

гейтится хостом — читается как 0 и никогда не срабатывает там, где экранной клавиатуры нет (macOS, десктоп, аппаратные клавиатуры).

clipboard

TypeScript
clipboard.write(text: string): void         // выстрелил и забыл
clipboard.read(): Promise<string>           // отклоняется: нет доступа / отказано / нечего читать

write кладёт текст в системный буфер обмена (тихий no-op там, где буфера обмена нет). read асинхронный, потому что веб-хост гейтит его за запросом разрешения; ожидай отклонение и обрабатывай его:

TypeScript
clipboard.write(inviteCode)
toast('Copied!')

try { input.value = await clipboard.read() }
catch { toast('Nothing to paste') }

openURL

TypeScript
openURL(url: string): void

Открой url в системном браузере / внешнем обработчике — выстрелил и забыл, как share. Используй для «Условий использования», «Оцените нас», ссылок mailto:. Тихий no-op на хостах, где такого нет (headless).

TypeScript
UIButton('Privacy policy').onClick(() => openURL('https://le.codes/privacy'))

Смотрите также

  • Устройствоdevice.online + события online/offline (связность).
  • Geolocation — останавливай watch'и в pause.
  • Хранилище — что сохранять, когда срабатывает pause.