Develop
Develop
Select your platform

Focus showcase - Data persistence and state management

Updated: Sep 29, 2025

Store multiple room configurations

To maintain application state between sessions, use Android Shared Preferences (or an equivalent) to save data that persists after the application has been closed.
Focus enables the creation of multiple objects, and each project requires storing various data types locally on your device. These include the position and rotation of spatial objects, states, associated text, and other properties. While Android Shared Preferences can be used for this purpose, SQLite may be a better solution for more complex data structures.
To add a SQLite database to your project you can follow this Android Developer SQLite Guide.
Focus has implemented a DatabaseManager class to create, save, and retrieve data from a database.
There are five tables with different elements and attributes: Projects, Unique Assets, Tools, Sticky Notes and Tasks. Implemented methods include the DatabaseManager to create, get, update, and delete the elements in these tables.

Use components to save and retrieve object data and state

In Focus, the elements are split into two categories:
  • Unique Assets: Elements that are unique in the scene and cannot be deleted, like the Clock, Speaker, Tasks Panel, and AI Exchange Panel.
  • Tools: Elements the user can create and delete, like Boards, Sticky Notes, Tasks, Labels, Arrows, Shapes, Stickers, and Timers.
The database must be updated whenever these elements change state or position.
ECS is the core of Spatial SDK. To identify different objects in Focus, there are three custom components to attach to the entities:
  • UniqueAssetComponent: ./app/src/main/java/com/meta/theelectricfactory/focus/UniqueAssetComponent.kt
  • ToolComponent: ./app/src/main/java/com/meta/theelectricfactory/focus/ToolComponent.kt
  • TimeComponent: ./app/src/main/java/com/meta/theelectricfactory/focus/TimeComponent.kt
To create a Component, your class must inherit from ComponentBase().

Unique Asset component

class UniqueAssetComponent (
    uuid: Int? = 0,
    type: AssetType = AssetType.CLOCK ) : ComponentBase() {

    var uuid by IntAttribute("uuid", R.id.UniqueAssetComponent_uuid, this, uuid)
    var type by EnumAttribute("type", R.id.UniqueAssetComponent_type, this, AssetType::class.java,  type)

    override fun typeID(): Int {
        return UniqueAssetComponent.id
    }
    companion object : ComponentCompanion {
        override val id = R.id.UniqueAssetComponent_class
        override val createDefaultInstance = { UniqueAssetComponent() }
    }
}
You must register your component in your activity to use it:
//ImmersiveActivity.kt
componentManager.registerComponent<UniqueAssetComponent>(UniqueAssetComponent.Companion)

Create a helper system to update objects’ positions

Focus uses a helper system that updates the position and rotation of objects in a database. It uses Spatial SDK systems and queries to check if an object has been grabbed and updates its position and rotation accordingly.
  1. Create a class that inherits from SystemBase.
  2. Register the system in your activity.
  3. In the execute function, create a query to pull data from the data model, and get all elements that have a UniqueAsset or Tool component.
  4. Check if any element in the lists has been grabbed. If so, update the position and rotation of the element in the database.
  5. Consider running this process periodically to improve performance.
// ImmersiveActivity.kt
systemManager.registerSystem(DatabaseUpdateSystem())

// DatabaseUpdateSystem.kt
class DatabaseUpdateSystem : SystemBase() {
    private var lastTime = System.currentTimeMillis()

    override fun execute() {

        if (ImmersiveActivity.instance.get()?.appStarted == false) return

        val currentTime = System.currentTimeMillis()

        // if there is no current project, we don't update database
        if (ImmersiveActivity.instance.get()?.currentProject == null) {
            lastTime = currentTime
        }

        // Check if objects are being moved and save new position
        // We do this each 0.2 seconds to improve performance
        val deltaTime = (currentTime - lastTime) / 1000f
        if (deltaTime > 0.2) {
            lastTime = currentTime

            // Update pose of tool assets
            val tools = Query.where { has(ToolComponent.id) }
            for (entity in tools.eval()) {
                val asset = entity.getComponent<ToolComponent>()
                val pose = entity.getComponent<Transform>().transform
                val isGrabbed = entity.getComponent<Grabbable>().isGrabbed

                if (isGrabbed) {
                    if (asset.type != AssetType.TIMER) ImmersiveActivity.instance.get()?.DB?.updateAssetPose(asset.uuid, asset.type, pose)
                }
            }

            // Update pose of unique assets
            val uniqueAssets = Query.where { has(UniqueAssetComponent.id) }
            for (entity in uniqueAssets.eval()) {
                val uniqueAsset = entity.getComponent<UniqueAssetComponent>()
                val pose = entity.getComponent<Transform>().transform
                val isGrabbed = entity.getComponent<Grabbable>().isGrabbed

                if (isGrabbed) ImmersiveActivity.instance.get()?.DB?.updateUniqueAsset(uniqueAsset.uuid, pose)
            }
        }
    }
}
You can find the complete code in DatabaseUpdateSystem.kt.

Detect keyboard events

You can detect keyboard events in Focus, like when a user has finished writing text. This is useful for updating text in the database. To do this, follow these steps:
  1. Add two properties to the EditText in the .XML file: android:inputType=”text” and android:imeOptions=”actionDone”.
  2. Set an action listener to the EditText using setOnEditorActionListener to perform an action once the user finishes writing.
  3. For longer texts that span multiple lines, add a TextWatcher to the EditText to detect when the user stops writing.
  4. Use the addEditTextListeners function from Utils.kt to add these listeners to any EditText and choose whether to update the text when the action is done, or when the user stops writing.
<!-- .xml file -->
<EditText
…
    android:inputType="text"
    android:imeOptions="actionDone" />
// Utils.kt
fun addEditTextListeners(editText: EditText?, onComplete: () -> (Unit), updateWithoutEnter:Boolean = false ) {

    // Detect Enter keyboard (Done) to perform action
    if (!updateWithoutEnter) {
        var enterTime:Long = 0
        val waitingInterval:Long = (0.5f * 1000).toLong() // 0.5 seconds

        editText?.setOnEditorActionListener { v, actionId, event ->
            if (actionId == EditorInfo.IME_ACTION_DONE || event?.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_ENTER) {

                // To avoid multiple events to trigger the same action
                if (System.currentTimeMillis() - enterTime >= waitingInterval) {
                    enterTime = System.currentTimeMillis()
                    onComplete()
                    return@setOnEditorActionListener true
                }
            }
            false
        }
    // In cases that virtual keyboard doesn't have an Enter (Done) button, like in multiline texts, we wait 1 second after user finished writing to perform the action
    } else {
        var lastTextChangeTime:Long = 0
        val typingInterval:Long = 1 * 1000
        val handler = Handler(Looper.getMainLooper())

        editText?.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }

            // update lastTime if user is still writing
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                lastTextChangeTime = System.currentTimeMillis()
            }

            override fun afterTextChanged(s: Editable?) {
                handler.postDelayed({
                    // Wait to perform action
                    if (System.currentTimeMillis() - lastTextChangeTime >= typingInterval) {
                        onComplete()
                    }
                }, typingInterval)
            }
        })
    }
}
Did you find this page helpful?
Thumbs up icon
Thumbs down icon