Recentemente, escrevi sobre a criação de aplicativos de desktop com Couchbase Lite e JavaFX. Conforme demonstrado, o Couchbase é uma excelente solução para armazenamento e sincronização de dados em um aplicativo de desktop devido ao Java SDK disponível. No entanto, sei que o JavaFX não é para todos.
Há outra estrutura semelhante para a criação de aplicativos de desktop em Java. Ela se chama Gluon e também oferece suporte para aplicativos Android e iOS. No entanto, neste exemplo, estamos analisando estritamente o desktop.
Veremos como criar um aplicativo de desktop Gluon usando praticamente o mesmo código encontrado em nosso exemplo anterior de JavaFX.
Os requisitos
Há alguns requisitos para criar um aplicativo Gluon que use o Couchbase.
- JDK 1.7+
- IntelliJ IDEA
- Gateway de sincronização do Couchbase
Normalmente, não faço disso um requisito, mas é muito mais fácil criar um aplicativo Gluon com um IDE como o IntelliJ, por isso ele está na lista. Há um plug-in para o IntelliJ que criará um projeto Gluon com o Gradle e tudo o que você precisa.
Embora o Couchbase Sync Gateway não seja realmente um requisito, ele é necessário se você quiser adicionar suporte à sincronização entre seu aplicativo e o Couchbase Server/outras plataformas e dispositivos.
Criação de um novo projeto Gluon
Se você decidir usar o IntelliJ para criar seu projeto, certifique-se de que já tenha feito o download do plug-in Gluon, conforme descrito aqui.
Usando o IntelliJ, crie um novo projeto, mas opte por criar um projeto Gluon Desktop - Projeto de múltiplas visualizações com FXML conforme mostrado abaixo.

Em última análise, cabe a você decidir o que fazer a partir daqui, mas para ficar o mais próximo possível deste guia, dê ao seu projeto um com.couchbaselabs nome do pacote e glúon classe principal.

Tudo o que vem a seguir pode ser deixado como padrão, pois faremos apenas um aplicativo de duas páginas com o Gluon. Quando terminarmos, esperamos ter uma estrutura de arquivos e diretórios parecida com a seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
gradle wrapper src main java com couchbaselabs views PrimaryPresenter.java PrimaryView.java SecondaryPresenter.java SecondaryView.java CouchbaseSingleton.java Todo.java gluon.java resources couchbaselabs views primary.fxml primary.css secondary.fxml secondary.css style.css icon.png build.gradle gradlew gradlew.bat |
Você notará que eu criei alguns arquivos extras, como CouchbaseSingleton.java e Todo.java.
Essencialmente, temos exibições XML e controladores para acompanhar essas exibições. Isso é muito semelhante ao que vimos em um Aplicativo JavaFX. Quando se trata de projetar essas exibições, temos algumas opções. Podemos usar XML bruto ou podemos usar Criador de cenas. Agora, esse SceneBuilder não deve ser confundido com o JavaFX SceneBuilder. Eu cometi esse erro e fiquei batendo cabeça por um bom tempo. A versão que queremos oferecerá suporte a aplicativos Gluon.
Antes de começarmos a adicionar o código do aplicativo, devemos adicionar nossas dependências ao arquivo Gradle do projeto. Se você não estiver familiarizado com o Gradle, ele faz o mesmo trabalho que o Maven ou o Ant. A sintaxe é diferente, mas eu a considero um pouco mais limpa. Abra o arquivo build.gradle e inclua o seguinte código:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
buildscript { repositories { jcenter() } dependencies { classpath 'org.javafxports:jfxmobile-plugin:1.0.8' } } apply plugin: 'org.javafxports.jfxmobile' repositories { jcenter() maven { url 'https://nexus.gluonhq.com/nexus/content/repositories/releases' } } mainClassName = 'com.couchbaselabs.gluon' dependencies { compile 'com.gluonhq:charm:3.0.0' compile 'com.couchbase.lite:couchbase-lite-java:1.3.0' desktopRuntime 'com.gluonhq:charm-desktop:3.0.0' } |
O que é particularmente importante aqui são as dependências:
|
1 2 3 4 5 6 7 |
dependencies { compile 'com.gluonhq:charm:3.0.0' compile 'com.couchbase.lite:couchbase-lite-java:1.3.0' desktopRuntime 'com.gluonhq:charm-desktop:3.0.0' } |
Isso incluirá a biblioteca Couchbase Lite, bem como o tempo de execução do aplicativo de desktop para o Gluon.
Com o projeto pronto, podemos começar a desenvolver o aplicativo.
Projetando a camada de dados do Couchbase
Ao trabalhar com o Couchbase, é uma boa ideia criar uma instância única dele. Isso significa que usaremos a mesma instância aberta durante todo o aplicativo, até decidirmos fechá-la.
Abra o arquivo src/main/java/com/couchbaselabs/CouchbaseSingleton.java e inclua o seguinte código:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
package com.couchbaselabs; import com.couchbase.lite.*; import com.couchbase.lite.replicator.Replication; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class CouchbaseSingleton { private Manager manager; private Database database; private Replication pushReplication; private Replication pullReplication; private static CouchbaseSingleton instance = null; private CouchbaseSingleton() { try { this.manager = new Manager(new JavaContext("data"), Manager.DEFAULT_OPTIONS); this.database = this.manager.getDatabase("fx-project"); View todoView = database.getView("todos"); todoView.setMap(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { emitter.emit(document.get("_id"), document); } }, "1"); } catch (Exception e) { e.printStackTrace(); } } public static CouchbaseSingleton getInstance() { if(instance == null) { instance = new CouchbaseSingleton(); } return instance; } public Database getDatabase() { return this.database; } public void startReplication(URL gateway, boolean continuous) { this.pushReplication = this.database.createPushReplication(gateway); this.pullReplication = this.database.createPullReplication(gateway); this.pushReplication.setContinuous(continuous); this.pullReplication.setContinuous(continuous); this.pushReplication.start(); this.pullReplication.start(); } public void stopReplication() { this.pushReplication.stop(); this.pullReplication.stop(); } public Todo save(Todo todo) { Map<String, Object> properties = new HashMap<String, Object>(); Document document = this.database.createDocument(); properties.put("type", "todo"); properties.put("title", todo.getTitle()); properties.put("description", todo.getDescription()); try { todo.setDocumentId(document.putProperties(properties).getDocument().getId()); } catch (Exception e) { e.printStackTrace(); } return todo; } public ArrayList query() { ArrayList results = new ArrayList(); try { View todoView = this.database.getView("todos"); Query query = todoView.createQuery(); QueryEnumerator result = query.run(); Document document = null; for (Iterator it = result; it.hasNext(); ) { QueryRow row = it.next(); document = row.getDocument(); results.add(new Todo(document.getId(), (String) document.getProperty("title"), (String) document.getProperty("description"))); } } catch (Exception e) { e.printStackTrace(); } return results; } } |
Se você viu o aplicativo JavaFX que criei anteriormente, perceberá que esse singleton é o mesmo entre os dois projetos. Você pode até usar uma versão semelhante para Android.
No CouchbaseSingleton estamos criando e abrindo um banco de dados local chamado projeto fx. Esse banco de dados será usado em todo o aplicativo. Também estamos criando nossa visualização do Couchbase Lite para consulta. Essa todos emitirá um par de valores-chave de id de documento e documento para cada documento no banco de dados local.
O método do construtor é privado, o que significa que não queremos que o usuário possa instanciar um objeto a partir dele. Em vez disso, queremos usar um método estático getInstance para realizar o trabalho.
Embora não vamos nos preocupar com a replicação até o final do guia, queremos estabelecer a base. A startReplication nos permitirá definir a sincronização bidirecional com um Sync Gateway e o método stopReplication nos permitirá interromper a replicação, talvez quando o aplicativo for fechado.
Agora temos nossas funções para salvar e carregar dados.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public Todo save(Todo todo) { Map<String, Object> properties = new HashMap<String, Object>(); Document document = this.database.createDocument(); properties.put("type", "todo"); properties.put("title", todo.getTitle()); properties.put("description", todo.getDescription()); try { todo.setDocumentId(document.putProperties(properties).getDocument().getId()); } catch (Exception e) { e.printStackTrace(); } return todo; } |
No salvar estamos aceitando um método personalizado Todo objeto. Na verdade, esse objeto contém apenas uma identificação, um título e uma descrição. A classe tem a seguinte aparência:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package com.couchbaselabs; import java.util.*; public class Todo { private String documentId; private String title; private String description; public Todo(String documentId, String title, String description) { this.documentId = documentId; this.title = title; this.description = description; } public Todo(String title, String description) { this.documentId = UUID.randomUUID().toString(); this.title = title; this.description = description; } public void setDocumentId(String documentId) { this.documentId = documentId; } public String getDocumentId() { return this.documentId; } public String getTitle() { return this.title; } public String getDescription() { return this.description; } } |
A classe acima é encontrada no src/main/java/com/couchbaselabs/Todo.java arquivo. O que estamos fazendo é, na verdade, pegar o objeto e adicioná-lo como propriedades a um documento NoSQL do Couchbase. Depois de salvar o documento e obter um ID, retornamos o mesmo documento com o ID incluído.
A função de consulta executará a exibição que criamos anteriormente e adicionará cada um dos itens de resultado a uma matriz de Todo encerrando nosso banco de dados singleton.
Criação de uma visualização para dados de listagem
Vamos criar um aplicativo que usa várias exibições do Gluon em vez de tentar colocar tudo na mesma exibição. Isso não deve ser confundido com as exibições do Couchbase Lite, que tratam de dados e não de interface do usuário.
A exibição padrão será a primeira exibida quando iniciarmos o aplicativo. Essa visualização mostrará uma lista de todos os nossos elementos de todo. Se não estiver usando o SceneBuilder, a marcação XML encontrada em src/main/resources/com/couchbaselabs/views/primary.fxml seria parecido com o seguinte:
|
1 2 3 4 5 6 |
<!--?xml version="1.0" encoding="UTF-8"?--> <!--?import com.gluonhq.charm.glisten.mvc.View?--> <!--?import javafx.scene.control.ListView?--> <!--?import javafx.scene.layout.BorderPane?--> |
|
1 2 3 |
A visualização resultante será parecida com a seguinte:

Você verá na imagem que há uma barra de navegação com um botão, mas ela não aparece no layout XML. Em vez disso, o layout contém apenas a exibição de lista. No entanto, o XML faz referência ao nosso src/main/java/com/couchbaselabs/views/PrimaryPresenter.java file. Esse é o arquivo em que definimos não apenas a barra de navegação, mas também qualquer lógica que potencialize a exibição específica.
O src/main/java/com/couchbaselabs/views/PrimaryPresenter.java terá muita semelhança com o nosso projeto JavaFX, com as diferenças no componente de navegação.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
package com.couchbaselabs.views; import com.couchbaselabs.CouchbaseSingleton; import com.couchbaselabs.Todo; import com.couchbase.lite.Database; import com.couchbase.lite.Document; import com.couchbaselabs.gluon; import com.gluonhq.charm.glisten.application.MobileApplication; import com.gluonhq.charm.glisten.control.AppBar; import com.gluonhq.charm.glisten.mvc.View; import com.gluonhq.charm.glisten.visual.MaterialDesignIcon; import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.util.Callback; public class PrimaryPresenter { private CouchbaseSingleton couchbase; @FXML private View primary; @FXML private ListView fxListView; public void initialize() { try { this.couchbase = CouchbaseSingleton.getInstance(); fxListView.getItems().addAll(this.couchbase.query()); this.couchbase.getDatabase().addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { for(int i = 0; i < event.getChanges().size(); i++) { final Document retrievedDocument = couchbase.getDatabase().getDocument(event.getChanges().get(i).getDocumentId()); Platform.runLater(new Runnable() { @Override public void run() { int documentIndex = indexOfByDocumentId(retrievedDocument.getId(), fxListView.getItems()); for (int j = 0; j < fxListView.getItems().size(); j++) { if (((Todo) fxListView.getItems().get(j)).getDocumentId().equals(retrievedDocument.getId())) { documentIndex = j; break; } } if (retrievedDocument.isDeleted()) { if (documentIndex > -1) { fxListView.getItems().remove(documentIndex); } } else { if (documentIndex == -1) { fxListView.getItems().add(new Todo(retrievedDocument.getId(), (String) retrievedDocument.getProperty("title"), (String) retrievedDocument.getProperty("description"))); } else { fxListView.getItems().remove(documentIndex); fxListView.getItems().add(new Todo(retrievedDocument.getId(), (String) retrievedDocument.getProperty("title"), (String) retrievedDocument.getProperty("description"))); } } } }); } } }); } catch (Exception e) { e.printStackTrace(); } fxListView.setCellFactory(new Callback<ListView, ListCell>() { @Override public ListCell call(ListView p) { ListCell cell = new ListCell() { @Override protected void updateItem(Todo t, boolean bln) { super.updateItem(t, bln); if (t != null) { setText(t.getTitle()); } } }; return cell; } }); primary.showingProperty().addListener((obs, oldValue, newValue) -> { if (newValue) { AppBar appBar = MobileApplication.getInstance().getAppBar(); appBar.setTitleText("Couchbase Todo - List"); appBar.getActionItems().add(MaterialDesignIcon.ADD.button(e -> MobileApplication.getInstance().switchView(gluon.SECONDARY_VIEW) )); } }); } private int indexOfByDocumentId(String needle, ObservableList haystack) { int result = -1; for(int i = 0; i < haystack.size(); i++) { if(haystack.get(i).getDocumentId().equals(needle)) { result = i; break; } } return result; } } |
No arquivo acima, temos a propriedade de exibição de lista vinculada à exibição de lista real no XML. O código que realmente importa, no entanto, é o código encontrado no arquivo inicializar método. Nele, fazemos três coisas fundamentais.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
fxListView.setCellFactory(new Callback<ListView, ListCell>() { @Override public ListCell call(ListView p) { ListCell cell = new ListCell() { @Override protected void updateItem(Todo t, boolean bln) { super.updateItem(t, bln); if (t != null) { setText(t.getTitle()); } } }; return cell; } }); |
No código acima, definimos como os dados aparecerão na lista. Por padrão, ele aceita apenas dados de cadeia de caracteres, portanto, nós o substituímos para obter o título do nosso Todo objetos.
|
1 2 3 4 5 6 7 8 9 10 11 |
primary.showingProperty().addListener((obs, oldValue, newValue) -> { if (newValue) { AppBar appBar = MobileApplication.getInstance().getAppBar(); appBar.setTitleText("Couchbase Todo - List"); appBar.getActionItems().add(MaterialDesignIcon.ADD.button(e -> MobileApplication.getInstance().switchView(gluon.SECONDARY_VIEW) )); } }); |
No listener acima, definimos o título da nossa barra de navegação e o botão. Quando o botão for pressionado, a exibição será alterada para a exibição secundária.
Por fim, resta-nos executar a consulta inicial de dados e preencher a lista, além de ouvir os novos dados à medida que eles chegam. Se houver alterações, elas serão iteradas e os indicadores serão revisados em cada documento alterado. Se houver um indicador excluído, os dados serão removidos do modo de exibição de lista. Se houver uma alteração, os dados do modo de exibição de lista serão removidos e, em seguida, substituídos. Caso contrário, os dados serão apenas adicionados. Como o ouvinte opera em um thread em segundo plano, as alterações na interface do usuário devem ser feitas dentro do thread Plataforma.runLater.
Isso nos leva à segunda e última visão.
Criação de uma visualização para salvar dados
A segunda visualização terá um formulário e será responsável pela entrada do usuário a ser adicionada ao banco de dados e exibida na visualização anterior. A marcação XML que alimenta essa visualização terá a seguinte aparência:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
<!--?xml version="1.0" encoding="UTF-8"?--> <!--?import com.gluonhq.charm.glisten.mvc.View?--> <!--?import javafx.geometry.Insets?--> <!--?import javafx.scene.control.TextArea?--> <!--?import javafx.scene.control.TextField?--> <!--?import javafx.scene.layout.BorderPane?--> <!--?import javafx.scene.layout.VBox?--> <textarea> </children> </VBox> </top> <padding> <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> </padding> </View> </code> </pre> <p>The XML above is found in the <strong>src/main/resources/com/couchbaselabs/views/secondary.fxml</strong> file and the view itself looks like the following:</p> <p><img src="/wp-content/original-assets/2016/october/using-couchbase-lite-in-a-java-gluon-application/cb-gluon-desktop-4.png" /></p> <p>Notice that there are two <code>TextField</code> inputs. They will be important in the <strong>src/main/java/com/couchbaselabs/views/SecondaryPresenter.java</strong> file referenced in the XML. This file contains the following code:</p> <pre> <code> package com.couchbaselabs.views; import com.couchbaselabs.CouchbaseSingleton; import com.couchbaselabs.Todo; import com.gluonhq.charm.glisten.animation.BounceInRightTransition; import com.gluonhq.charm.glisten.application.MobileApplication; import com.gluonhq.charm.glisten.control.AppBar; import com.gluonhq.charm.glisten.layout.layer.FloatingActionButton; import com.gluonhq.charm.glisten.mvc.View; import com.gluonhq.charm.glisten.visual.MaterialDesignIcon; import javafx.fxml.FXML; import javafx.scene.control.Alert; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; public class SecondaryPresenter { private CouchbaseSingleton couchbase; @FXML private View secondary; @FXML private TextField fxTitle; @FXML private TextArea fxDescription; public void initialize() { this.couchbase = CouchbaseSingleton.getInstance(); secondary.setShowTransitionFactory(BounceInRightTransition::new); secondary.getLayers().add(new FloatingActionButton(MaterialDesignIcon.SAVE.text, e -> save() )); secondary.showingProperty().addListener((obs, oldValue, newValue) -> { if (newValue) { AppBar appBar = MobileApplication.getInstance().getAppBar(); appBar.setTitleText("Couchbase Todo - Create"); } }); } private void save() { if(!fxTitle.getText().equals("") && !fxDescription.getText().equals("")) { couchbase.save(new Todo(fxTitle.getText(), fxDescription.getText())); fxTitle.setText(""); fxDescription.setText(""); MobileApplication.getInstance().switchToPreviousView(); } else { Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("Missing Information"); alert.setHeaderText(null); alert.setContentText("Both a title and description are required for this example."); alert.showAndWait(); } } } </code> </pre> <p>The input fields are mapped to this controller, but what really matters here is the code for adding the floating action button and setting the navigation bar title.</p> <pre> <code> secondary.getLayers().add(new FloatingActionButton(MaterialDesignIcon.SAVE.text, e -> save() )); secondary.showingProperty().addListener((obs, oldValue, newValue) -> { if (newValue) { AppBar appBar = MobileApplication.getInstance().getAppBar(); appBar.setTitleText("Couchbase Todo - Create"); } }); </code> </pre> <p>When the floating action button is clicked, the <code>save</code> method is called. In the <code>save</code> method we check to make sure the input fields are not blank and if they aren't, save the data and navigate backwards in the stack to the previous view.</p> <h2>Syncing Data with Couchbase Sync Gateway</h2> <p>Up until now, every part of our Gluon application was built for offline local use. However, including synchronization support into the mix is not only useful, but incredibly easy.</p> <p>At this point I'm going to assume you've downloaded and installed Couchbase Sync Gateway. Before we run it, we need to create a configuration file. Create a JSON file with the following:</p> <pre> <code> { "log":["CRUD+", "REST+", "Changes+", "Attach+"], "databases": { "fx-example": { "server":"walrus:", "sync":` function (doc) { channel (doc.channels); } `, "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } } } } </code> </pre> <p>The above configuration file is one of the most simplest you can make for Sync Gateway. You're creating a partition called <strong>fx-example</strong> within the in-memory database <strong>walrus</strong> and you're accepting all documents from everyone with no read or write permissions.</p> <p>Running this configuration with Sync Gateway won't get us very far yet because we haven't activated sync support in our application. Open the project's <strong>src/main/java/com/couchbaselabs/gluon.java</strong> file and include the following:</p> <pre> <code> package com.couchbaselabs; import com.couchbaselabs.views.PrimaryView; import com.couchbaselabs.views.SecondaryView; import com.gluonhq.charm.glisten.application.MobileApplication; import com.gluonhq.charm.glisten.visual.Swatch; import javafx.scene.Scene; public class gluon extends MobileApplication { public static final String PRIMARY_VIEW = HOME_VIEW; public static final String SECONDARY_VIEW = "Secondary View"; public CouchbaseSingleton couchbase; @Override public void init() { addViewFactory(PRIMARY_VIEW, () -> new PrimaryView(PRIMARY_VIEW).getView()); addViewFactory(SECONDARY_VIEW, () -> new SecondaryView(SECONDARY_VIEW).getView()); } @Override public void postInit(Scene scene) { Swatch.BLUE.assignTo(scene); scene.getStylesheets().add(gluon.class.getResource("style.css").toExternalForm()); try { this.couchbase = CouchbaseSingleton.getInstance(); this.couchbase.startReplication(new URL("https://localhost:4984/fx-example/"), true); } catch (Exception e) { e.printStackTrace(); } } } </code> </pre> <p>Really we only care about the <code>startReplication</code> line in the <code>postInit</code> method. Once we call it, replication will happen in both directions, continuously.</p> <h2>Conclusion</h2> <p>You just saw how to create a Java desktop application with Gluon and Couchbase. Using Gradle you can build and run the application and with a few revisions it can be converted to Android as well.</p> <p>The full source code to this project can be found on GitHub <a href="https://github.com/couchbaselabs/couchbase-lite-gluon-example">here</a>.</p> </textarea> |