Persisting State with DukeScript

I’ve been asked a couple of times, if it’s possible to store data on the device via DukeScript - for example in order to store UI state. We’re currently exploring how to automate this tracking, so you would only need to register your ViewModel once for automated persistence. A PersistenceManager would then take care of persisting the model on every change of a Model Property. For Performance reasons there would be a custom debounce threshold you can set to make sure changes are only persisted when input events have comleted.

Until we offer this as an API (Update: as of February 2017, there is an easy way to get started via Maven archetype 0.17), you might want a specific solution for your Application right now.

So here’s a starting point:

public class StorageManager {

    private static Storage LOCAL = new Storage() {

        @Override
        public void put(String key, String val) {
            StorageManager.put_impl(key, val);
        }

        @Override
        public String get(String key) {
            return StorageManager.get_impl(key);
        }

    };

    private static Storage FALLBACK = new Storage() {

        @Override
        public void put(String key, String val) {
            Preferences pref = Preferences.userNodeForPackage(StorageManager.class);
            pref.put(key, val);
        }

        @Override
        public String get(String key) {
            Preferences pref = Preferences.userNodeForPackage(StorageManager.class);
            return pref.get(key, "");
        }
    };

    private static Storage EMPTY = new Storage() {

        @Override
        public void put(String key, String val) {

        }

        @Override
        public String get(String key) {
            return "";
        }
    };

    public interface Storage {

        public void put(String key, String val);

        public String get(String key);
    }

    public static Storage getStorage() {
        if (supportsLocalStorage()) {
            return LOCAL;
        } else if (supportsPreferences()) {
            return FALLBACK;
        }
        return EMPTY;
    }

    @JavaScriptBody(args = {}, body = "try {\n"
            + "    return 'localStorage' in window && window['localStorage'] !== null;\n"
            + "  } catch (e) {\n"
            + "    return false;\n"
            + "  }")
    private static native boolean supportsLocalStorage();

    private static boolean supportsPreferences() {
        try {
            Preferences userNodeForPackage = Preferences.userNodeForPackage(StorageManager.class);
            userNodeForPackage.put("dummy_probably_very_unlikely_s0mbody_U5e5_th1s_key", "-1");

            return true;
        } catch (Exception e) {
            return false;
        }
    }

    @JavaScriptBody(args = {"key"}, body = "if(localStorage){"
            + "  var obj = localStorage[key]; "
            + "  return obj;"
            + "}"
            + "return '';")
    public static native String get_impl(String key);

    @JavaScriptBody(args = {"key", "val"}, body = "var store = window['localStorage'];if (store) localStorage[key]=val;")
    public static native void put_impl(String key, String val);
}

With this code you can store Strings, which is good enough, because ViewModels can be serialized to JSON. So you can easily read and write your data:

// store the data as JSON
StorageManager.getStorage().put("test", model.toString()); 
// parse JSON
String test = StorageManager.getStorage().get("test");
ViewModel vm = new ViewModel();
InputStream inputStream = new ByteArrayInputStream(test.getBytes(StandardCharsets.UTF_8));
try {
    vm = Models.parse(BrwsrCtx.findDefault(TaskList.class), ViewModel.class, inputStream);
} catch (IOException ex) {
    taskList = new TaskList();
}
            
}
Models.applyBindings(vm, "body");

This simple solution requires you to track changes in the model yourself and serialize the data.

By the way: A happy new Year to all friends of DukeScript!