110 lines
7.2 KiB
Markdown
110 lines
7.2 KiB
Markdown
|
---
|
|||
|
layout: post
|
|||
|
title: "💾 Saving and restoring browser session state"
|
|||
|
date: 2019-02-18 14:35:00 +0200
|
|||
|
author: sebastian
|
|||
|
---
|
|||
|
|
|||
|
Losing open tabs in a browser can be a painful experience for the user. By itself [SessionManager](https://mozac.org/api/mozilla.components.browser.session/-session-manager/) keeps its state only in memory. This means that something as simple as switching to a different app can cause Android to [kill the browser app's process and reclaim its resources](https://developer.android.com/guide/components/activities/process-lifecycle). The result of that: The next time the user switches back to the browser app they start with a fresh browser without any tabs open.
|
|||
|
|
|||
|
[Mozilla's Android Components](https://mozac.org/) come with two implementations of session storage and helpers to write your own easily.
|
|||
|
|
|||
|
## SessionStorage
|
|||
|
|
|||
|
The [SessionStorage](https://mozac.org/api/mozilla.components.browser.session.storage/-session-storage/) class that comes with the [browser-session](https://github.com/mozilla-mobile/android-components/tree/main/components/browser/session) component saves the state as a file on disk (using [AtomicFile](https://developer.android.com/reference/android/util/AtomicFile) under the hood). It can be used for a browser that wants to have a single state that gets saved and restored (like Fennec or Chrome).
|
|||
|
|
|||
|
```kotlin
|
|||
|
val sessionStorage SessionStorage(applicationContext, engine)
|
|||
|
|
|||
|
val sessionManager = sessionManager(engine).apply {
|
|||
|
sessionStorage.restore()?.let { snapshot -> restore(snapshot) }
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
ℹ️ Since restoring the last state requires a disk read, it is recommended to perform it off the main thread. This requires the app to gracefully handle the situation where the app starts without any sessions at first. [SessionManager](https://mozac.org/api/mozilla.components.browser.session/-session-manager/) will invoke [onSessionsRestored()](https://mozac.org/api/mozilla.components.browser.session/-session-manager/-observer/on-sessions-restored.html) on a registered [SessionManager.Observer](https://mozac.org/api/mozilla.components.browser.session/-session-manager/-observer/) after restoring has completed.
|
|||
|
|
|||
|
## SessionBundleStorage
|
|||
|
|
|||
|
Other than [SessionStorage](https://mozac.org/api/mozilla.components.browser.session.storage/-session-storage/) the [SessionBundleStorage](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/) implementation can save and restore from multiple states. State is saved as a Bundle in a database.
|
|||
|
|
|||
|
The storage is set up with a bundle lifetime. [SessionBundleStorage](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/) will only restore the last bundle if its lifetime has not expired. If there's no active bundle then a new empty bundle will be created to save the state.
|
|||
|
|
|||
|
```kotlin
|
|||
|
val engine: Engine = ...
|
|||
|
|
|||
|
val sessionStorage = SessionBundleStorage(
|
|||
|
applicationContext,
|
|||
|
bundleLifetime = Pair(1, TimeUnit.HOURS)
|
|||
|
|
|||
|
val sessionManager = sessionManager(engine).apply {
|
|||
|
// We launch a coroutine on the main thread. Once a snapshot has been restored
|
|||
|
// we want to continue with it on the main thread.
|
|||
|
GlobalScope.launch(Dispatchers.Main) {
|
|||
|
// We restore on the IO dispatcher to not block the main thread:
|
|||
|
val snapshot = async(Dispatchers.IO) {
|
|||
|
val bundle = sessionStorage.restore()
|
|||
|
// If we got a bundle then restore the snapshot from it
|
|||
|
bundle.restoreSnapshot(engine)
|
|||
|
}
|
|||
|
|
|||
|
// If we got a snapshot then restore it now:
|
|||
|
snapshot.await()?.let { sessionManager.restore(it) }
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
The storage comes with an [API](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/#functions) that allows apps to build UIs to list, restore, save and remove bundles.
|
|||
|
|
|||
|
![](/assets/images/blog/session-bundles.png)
|
|||
|
|
|||
|
## AutoSave
|
|||
|
|
|||
|
Knowing when to save state, by calling [SessionStorage.save()](https://mozac.org/api/mozilla.components.browser.session.storage/-session-storage/save.html) or [SessionBundleStorage.save()](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/save.html), can be complicated. Restoring an outdated state can be an as bad a user experience as restoring no state at all.
|
|||
|
|
|||
|
The [AutoSave](https://mozac.org/api/mozilla.components.browser.session.storage/-auto-save/) class is a helper for configuring automatic saving of the browser state - and you can use it with [SessionStorage](https://mozac.org/api/mozilla.components.browser.session.storage/-session-storage/) as well as [SessionBundleStorage](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/).
|
|||
|
|
|||
|
```kotlin
|
|||
|
sessionStorage.autoSave(sessionManager)
|
|||
|
// Automatically save the state every 30 seconds:
|
|||
|
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
|
|||
|
// Save the state when the app goes to the background:
|
|||
|
.whenGoingToBackground()
|
|||
|
// Save the state whenever sessions change (e.g. a new tab got added or a website
|
|||
|
// has completed loading).
|
|||
|
.whenSessionsChange()
|
|||
|
```
|
|||
|
|
|||
|
## Implementing your own SessionStorage
|
|||
|
|
|||
|
If neither [SessionStorage](https://mozac.org/api/mozilla.components.browser.session.storage/-session-storage/) nor [SessionBundleStorage](https://mozac.org/api/mozilla.components.feature.session.bundling/-session-bundle-storage/) satisfy the requirements of your app (e.g. you want to save the state in your favorite database or in a cloud-enabled storage) then it is possible to implement a custom storage.
|
|||
|
|
|||
|
The [AutoSave.Storage](https://mozac.org/api/mozilla.components.browser.session.storage/-auto-save/-storage/) interface from the [browser-session](https://github.com/mozilla-mobile/android-components/tree/main/components/browser/session) component defines the methods that are expected from a session storage. Technically it is not required to implement the interface if your app code is the only one interacting with the session store; but implementing the interface makes your implementation compatible with other components code. Specifically you can use [AutoSave](https://mozac.org/api/mozilla.components.browser.session.storage/-auto-save/) with any class implementing SessionStorage without any additional code.
|
|||
|
|
|||
|
The [SnapshotSerializer](https://mozac.org/api/mozilla.components.browser.session.storage/-snapshot-serializer/) class is helpful when translating snapshots from and to JSON.
|
|||
|
|
|||
|
```kotlin
|
|||
|
class MyCustomStorage(
|
|||
|
private val engine: Engine
|
|||
|
) : AutoSave.Storage {
|
|||
|
private val serializer = SnapshotSerializer()
|
|||
|
|
|||
|
override fun save(snapshot: SessionManager.Snapshot): Boolean {
|
|||
|
val json = serializer.toJSON(snapshot)
|
|||
|
|
|||
|
// TODO: Save JSON custom storage.
|
|||
|
|
|||
|
// Signal that save operation was successful:
|
|||
|
return true
|
|||
|
}
|
|||
|
|
|||
|
fun restore(): SessionManager.Snapshot {
|
|||
|
// TODO: Get JSON from custom storage.
|
|||
|
val json = ...
|
|||
|
|
|||
|
return serializer.fromJSON(engine, json)
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
ℹ️ For simplicity the implementation above does not handle [JSONException](ad https://developer.android.com/reference/org/json/JSONException.html) which can be thrown by [SnapshotSerializer](https://mozac.org/api/mozilla.components.browser.session.storage/-snapshot-serializer/).
|