The CloudLink Combined Storage App is a Gluon code sample. Refer to the Gluon website for a full list of Gluon code samples.

In this tutorial, we’ll explain how you can make use of and combine local and cloud storage with the Gluon CloudLink Data Storage service.

Before you start, be sure that you have checked the list of prerequisites, and you have also installed the Gluon plugin for your IDE. Otherwise follow these instructions.

Note: This tutorial will use the plugin for NetBeans, but it works as well on IntelliJ and Eclipse.

Code: The code for this project can be found in the samples repository at GitHub. The sample is located under the directory combinedstorage. The reader can clone this repository or create the entire project from scratch, based on the following steps.

Creating the project

This project can be created from scratch using the Gluon plugin on your favourite IDE. But since this is a follow up of the Notes and PushNotes apps, we’ll clone the original Notes sample instead and modify it to add new features. We’ll refactor the main package to com.gluonhq.combinedstorage and the application class to CombinedStorage. Make sure to do the necessary adjustments in the FXML (fx:controller), build.gradle, AndroidManifest.xml and Default-info.plist files.

Modifying the project

Let’s start modifying the default project to create our CombinedStorage application, based on the Notes sample.

For this sample, we’ll remove the Settings view and also the Filter layer.

The model package

The model package contains the Note and Model classes, exactly the same as in the Notes and PushNotes samples. We’ll override the Note equals method in order to compare lists later on.

The service

In the original Notes sample, we created a service that used Gluon CloudLink to persist the notes we created and/or edited locally on the device. On the other hand, the PushNotes sample added cloud persistence.

In this sample we’ll briefly show how both strategies can be combined and used together, starting by adding two DataClient objects:

Service.java
public class Service {

    private DataClient localDataClient;
    private DataClient cloudDataClient;

    @PostConstruct
    public void postConstruct() {
        localDataClient = DataClientBuilder.create()
                .operationMode(OperationMode.LOCAL_ONLY)
                .build();

        cloudDataClient = DataClientBuilder.create()
                .operationMode(OperationMode.CLOUD_FIRST)
                .build();
    }
}

With localDataClient we’ll target the local storage on our device, while cloudDataClient uses OperationMode.CLOUD_FIRST to indicate that data will be directly stored into and retrieved from Gluon CloudLink.

Each DataClient object uses its own DataProvider as an entry point to retrieve a GluonObservableList. For each of them, we need to supply a proper ListDataReader, a valid entity with the ability to read a list of objects. For that, the DataClient instance already has one method: createListDataReader(). All it needs is an identifier (NOTES), and the object class to be read (Note.class).

If we just want to use one or the other data client, we could just call:

Service.java
public class Service {

    private static final String NOTES = "notes-combined";

    private static final boolean CLOUD_STORAGE_ENABLED = true;

    private GluonObservableList<Note> notes;

    @PostConstruct
    public void postConstruct() {
        ...
        notes = retrieveNotes();
    }

    private GluonObservableList<Note> retrieveNotes() {
        if (CLOUD_STORAGE_ENABLED) {
            GluonObservableList<Note> internalCloudNotes = DataProvider.retrieveList(
                    cloudDataClient.createListDataReader(NOTES, Note.class, SyncFlag.LIST_WRITE_THROUGH, SyncFlag.OBJECT_WRITE_THROUGH)));

            internalCloudNotes.initializedProperty().addListener((obs, ov, nv) -> {
                if (nv) {
                    notes.set(internalCloudNotes);
                }
            });
            return internalCloudNotes;
        } else {
            GluonObservableList<Note> internalLocalNotes = DataProvider.retrieveList(
                    localDataClient.createListDataReader(NOTES, Note.class)));

            internalLocalNotes.initializedProperty().addListener((obs, ov, nv) -> {
                if (nv) {
                    notes.set(internalLocalNotes);
                }
            });
            return internalLocalNotes;
        }
    }
}

But instead, we want to combine both strategies, giving priority to the cloud in this case. So first we’ll try to retrieve the notes from the local storage (as this is faster and works reliably even with bad or inexistent connectivity), but then we’ll try to retrieve the notes from the cloud, and then we will synchronize both lists, adding those notes from the cloud that are missing locally, and removing the local notes that are not present in the cloud list.

Service.java
public class Service {

    private GluonObservableList<Note> retrieveNotes() {
        // Read local notes first
        GluonObservableList<Note> internalLocalNotes = DataProvider.retrieveList(
                localDataClient.createListDataReader(NOTES, Note.class,
                            SyncFlag.LIST_WRITE_THROUGH,
                            SyncFlag.OBJECT_WRITE_THROUGH));

        internalLocalNotes.initializedProperty().addListener((obs, ov, nv) -> {
            if (nv) {
                if (CLOUD_STORAGE_ENABLED) {
                    // Retrieve cloud notes
                    GluonObservableList<Note> internalCloudNotes = DataProvider.retrieveList(
                            cloudDataClient.createListDataReader(NOTES, Note.class,
                            SyncFlag.LIST_WRITE_THROUGH,
                            SyncFlag.OBJECT_WRITE_THROUGH));

                    internalCloudNotes.initializedProperty().addListener((obs2, ov2, nv2) -> {
                        if (nv2) {
                            // add to local new notes from cloud
                            for (Note cloudNote : internalCloudNotes) {
                                if (!internalLocalNotes.contains(cloudNote)) {
                                    internalLocalNotes.add(cloudNote);
                                }
                            }

                            // remove from local if it doesn't exist in the cloud
                            List<Note> toRemove = new ArrayList<>();
                            for (Note localNote : internalLocalNotes) {
                                if (!internalCloudNotes.contains(localNote)) {
                                    toRemove.add(localNote);
                                }
                            }
                            internalLocalNotes.removeAll(toRemove);

                            // bind content between local and cloud lists
                            Bindings.bindContent(internalCloudNotes, internalLocalNotes);
                        }
                    });
                }
            }
        });
        return internalLocalNotes;
    }

    public Note addNote(Note note) {
        notes.add(note);
        return note;
    }

    public void removeNote(Note note) {
        notes.remove(note);
    }

    public GluonObservableList<Note> getNotes() {
        return notes;
    }
}

Note that is just a possible combination of local and cloud storage. This strategy could be modified to give preference to local storage instead, or to create more complex scenarios.

Registering the app

The DataClient uses a key and a secret token that are specific for the application. Those tokens are used for signing the requests that are made to Gluon CloudLink. To obtain a key and secret, you need a valid subscription to Gluon CloudLink. You can get it here (there is also a 30-day free trial). Sign up and get a valid account on Gluon CloudLink and a link to access the Gluon Dashboard.

Open the Dashboard in your browser, and sign in using the Gluon account credentials provided to create the account.

Dashboard

Go to the Credentials link, and you will find a pair of application key/secret tokens. Click on the download button to download the file gluoncloudlink_config.json, and then store it under your project src/main/resources/ folder.

Configuration File

The content of the file is a JSON object with the key and secret that will grant access to Gluon CloudLink:

src/main/resources/gluoncloudlink_config.json
{
  "gluonCredentials": {
    "applicationKey" : "f88XXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "applicationSecret": "7fbXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
  }
}

Running the app

After updating the project, let’s run it on desktop and create our first note.

Notes View

We can verify now that the note is stored locally (under <user>./gluon/notes-combined), and also in the cloud running the Dashboard application in our browser. Sign in with your credentials and go to the Data Management link, you will be able to see the actual content of the notes list:

Dashboard Data view

Deploy on mobile

We can deploy now the app to an Android and/or iOS device.

Select from Tasks→android→androidInstall to deploy on Android, or Tasks→launch→launchIOSDevice to deploy on iOS. Once the app is deployed, open it.

Android app

When running the app, we’ll see an empty list first, as the local storage was empty, but immediately the note created before from the desktop app will be retrieved from the cloud and it will show up, and it will be also added to the local storage.

Now, if using the Dashboard we remove the note from the CloudLink, when we open the app we’ll see briefly the (local) note, until the lists are synchronized and the note is also removed locally.

Conclusion

During this tutorial we have accomplished several tasks after updating the existing Notes sample:

  • We have modified the Notes sample to add both local and cloud persistence with DataClient and GluonObservableList.

  • And by using the Dashboard web application we’ve been able to track the changes in those persisted lists and objects.

  • We have added a possible strategy for combining lists from local and cloud storage, giving preference to the data stored in the cloud.

If you have made it this far, congratulations, we hope this sample and the documentation was helpful to you! In case you have any questions, your first stop should be the Gluon support page, where you can access the CloudLink latest documentation. Gluon also recommends the community to support each other over at the Gluon StackOverflow page. Finally, Gluon offers commercial support as well, to kick start your projects.