11 KiB
Create Vitruvio Mobile Form
All messages shown to the user must be written in Portuguese.
Mobile forms are not desktop forms with a different skin. They use a different schema, a smaller component set, a different JavaScript runtime, and an explicit server↔client data contract. Read this whole skill before scaffolding — the mistakes here are not the desktop mistakes.
The four things that make mobile different
- Different schema. Root is
<mobile-forms>, namespacehttp://www.davinti.com.br/vitruvio/mobile-form, XSDvitruvio-mobile-form.xsd. - Only 18 components (vs 78 desktop). Do not assume a desktop widget exists on
mobile. The full list:
CheckBox, ComboBox, GoogleMapsField, HorizontalLayout, ImageLibraryByFieldValue, ImageWidget, Label, MaskedField, MoneyField, NumericField, OptionGroup, ProgressBarWidget, RatingStars, SignaturePadField, TabLayout, TextField, Toggle, VerticalLayout(plus structuralSubForm/ItemList). Read~/.local/share/vitruvio-platform/docs/components/mobile/<Component>.mdbefore using one. - Two JavaScript runtimes.
- Client-side (
initScript,discoveryScript, validators, component event scripts): modern React-Native JS — arrow functions, Promises,.then()/.catch()are fine and expected. Most APIs are async and return Promises. - Server-side (
<ServerSide><Bridge>execute(...)bodies): Rhino ES5, same rules as scripts/desktop. This is the only place with access to platform libs and services (libService,runtimeService, db, etc.).
- Client-side (
- Data is explicit. Nothing is auto-injected the way desktop process variables are. Every
piece of server data the form needs must be declared — via an
<Autoload>variable, a<QueryDataSource>(synced to the device, works offline), or fetched on demand from a named<Bridge>withvCommunicationService.executeOnServer(...).
Step 1 — Confirm you are inside a Vitruvio repo
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
If NOT_A_VITRUVIO_REPO, stop and tell the user to cd into the correct repo.
Step 2 — Determine variant and gather the data contract
Ask (only what is missing):
- Panel or process mobile form? (see Differences: panel vs process below)
- Key — the panel or process key (folder name).
- For a process: which
formKey(s) — they must match theactiviti:formKeyinprocesses/<key>/<key>.bpmn. - Which variables does the form need? Because nothing is auto-injected, ask explicitly:
- process variables to read (each becomes an
<Autoload><variable>and/or a Bridge call) - reference/lookup data (each becomes a
<QueryDataSource>backed by aqueries/*.sql) - any server-only logic / libs needed (each becomes a
<Bridge>)
- process variables to read (each becomes an
- Offline? Whether the data sources must be available without connectivity (affects
autoSyncOnInit/autoSyncOnDiscovery).
Differences: panel vs process
The schema, component set, ServerSide/Bridge mechanics and JS runtimes below are identical for both. Only these differ:
| Panel mobile form | Process mobile form | |
|---|---|---|
| File | panels/<key>/<key>-mobile.xml |
processes/<key>/<key>-mobile.xml |
| Root attribute | <mobile-forms> (no processKey) |
<mobile-forms processKey="<key>"> |
| Forms per file | one <form> |
one <form formKey> per BPMN activiti:formKey (must match) |
| Manifest | set forms.mobile and showInMobileList: true on the panel entry |
set forms.mobile (and optionally forms.mobileAlternative) on the process entry |
| Variables | use engine.getGlobalVariable / Autoload |
process variables are fetched server-side via a Bridge (runtimeService.getVariable) and/or declared in <Autoload> |
Filename: name files by the artifact key + suffix —
<key>-mobile.xml(and<key>-desktop.xml,<key>.bpmn). This keeps every form searchable by its key instead of dozens of identicalform-mobile.xmltabs. Older content usedform-mobile.xml/form_web_mobile.xml; the real path is whateverforms.mobilepoints to, so legacy files still work — but new scaffolds use<key>-mobile.xml.
Step 3 — Scaffold the file
Skeleton (process variant shown; for a panel drop processKey and use a single <form>)
<?xml version="1.0" encoding="UTF-8"?>
<mobile-forms processKey="<key>"
xmlns="http://www.davinti.com.br/vitruvio/mobile-form"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.davinti.com.br/vitruvio/mobile-form https://bitbucket.org/davinTI/vitruvio-xds/raw/master/vitruvio-mobile-form.xsd">
<form formKey="formColeta">
<name>Coleta</name>
<description>Etapa de coleta no app</description>
<!-- Runs when the form opens. CLIENT-SIDE: modern JS, async APIs return Promises. -->
<initScript language="JavaScript">
<![CDATA[
function run() {
// Autoloaded variables land in the global scope:
engine.getField('nomeLoja').setValue(engine.getGlobalVariable('nomeLoja'));
}
]]>
</initScript>
<!-- Optional: pull server data when the task is discovered (before the user opens it). -->
<discoveryScript language="JavaScript">
<![CDATA[
function run() {
var params = { id: execution.getProcessInstanceId() };
vCommunicationService.executeOnServer('bridgeNumeroCarga', params).then(result => {
var n = parseInt(result, 10);
execution.setVariable('numeroCarga', n).then(ok => {}).catch(err => {
console.log('Erro ao setar numeroCarga localmente', err);
});
}).catch(err => {
console.log('Erro ao coletar numeroCarga no server', err);
});
}
]]>
</discoveryScript>
<!-- Block completing the task when a rule is not met. -->
<validators>
<ScriptValidator execution="COMPLETE" language="JavaScript" id="validadorComplete">
<![CDATA[
function Validator() {
var msg;
this.getMessage = function() { return msg; };
this.isValid = function() {
if (engine.getField('confirma').getValue() == 'Sim') { return true; }
msg = 'Confirme a execução antes de finalizar.';
return false;
};
}
var validator = new Validator();
]]>
</ScriptValidator>
</validators>
<components>
<VerticalLayout spacing="true" margin="true" width="100%">
<TextField id="nomeLoja" type="string" caption="Loja" readOnly="true" />
<OptionGroup id="confirma" type="string" caption="Executou?" required="true">
<entry key="Sim" value="Sim" />
<entry key="Nao" value="Não" />
</OptionGroup>
<SignaturePadField id="assinatura" caption="Assinatura" />
</VerticalLayout>
</components>
<!-- Explicitly declared variables auto-injected into the form's scope on load. -->
<Autoload>
<variables autoInjectionScope="ENGINE_GLOBAL_SCOPE" autoPersist="true">
<variable>nomeLoja</variable>
<variable>numeroCarga</variable>
</variables>
</Autoload>
<!-- Everything the server provides to this form. -->
<ServerSide>
<!-- Named queries (queries/*.sql) synced to the device for offline lookups. -->
<DataSources>
<QueryDataSource key="qry_produtos_carga" autoSyncOnInit="true" autoSyncOnDiscovery="true" refreshInSeconds="60" />
</DataSources>
<!-- Server-side functions. Rhino ES5. Full access to libService / runtimeService / db.
Called from the client via vCommunicationService.executeOnServer('id', params). -->
<Bridges>
<Bridge language="JavaScript" id="bridgeNumeroCarga">
<![CDATA[
function execute(params) {
var numeroCarga = runtimeService.getVariable(params.id, 'numeroCarga');
return numeroCarga ? numeroCarga : -1;
}
]]>
</Bridge>
</Bridges>
</ServerSide>
</form>
</mobile-forms>
How libs and process variables reach the mobile form
The mobile app cannot call libService.loadScript(...) or read process variables directly —
those live on the server. The pattern is always declare a Bridge, call it from the client:
<!-- SERVER-SIDE (Rhino ES5): a lib used to build a barcode image -->
<Bridge language="JavaScript" id="imagemCodBarras">
<![CDATA[
function execute(codbarras) {
var generator = libService.loadScript('barcode-gen');
return ',' + generator.generateEAN13BarcodeImageAsBase64({ value: codbarras });
}
]]>
</Bridge>
// CLIENT-SIDE (modern JS): call the bridge, use the Promise result
vCommunicationService.executeOnServer('imagemCodBarras', codigoBarras).then(base64 => {
engine.getField('codigoImagem').setValue(base64);
}).catch(err => console.log('Erro no bridge imagemCodBarras', err));
Rules of thumb:
- Anything needing a platform lib, the database, or a platform service → put it in a
Bridge (server, ES5) and call it with
vCommunicationService.executeOnServer. - Reference/lookup tables the form reads repeatedly → a
QueryDataSourcebacked by aqueries/*.sql(works offline once synced). - Process variables the form needs → either declare them in
<Autoload>or fetch them in a Bridge viaruntimeService.getVariable(processInstanceId, 'varName')and store locally withexecution.setVariable(...). - Client-side
execution.getVariable(...)/execution.setVariable(...)are async and return Promises — use.then().
List-based entry: SubForm + ItemList
For "add many items" screens (collect a list of rows), use a SubForm with an <ItemList>:
<SubForm formKey="executarAcao">
<name>Executar AÇÃO</name>
<initScript language="JavaScript">
<![CDATA[ function run(apply) { if (apply) { apply(); } } ]]>
</initScript>
<ItemList addItemButtonCaption="Gravar na Lista" caption="Itens">
<property id="OBSERVACAO" caption="Observação" />
</ItemList>
<components>
<VerticalLayout width="100%" spacing="true" margin="true">
<!-- fields captured per item -->
</VerticalLayout>
</components>
</SubForm>
Validate list completeness with engine.getSubFormListSizeByStatus(function(size){ ... }, "all")
inside a COMPLETE validator.
Step 4 — Manifest
If run standalone, update the matching entry in vitruvio.json:
- Panel: set
forms.mobile = "panels/<key>/<key>-mobile.xml"andshowInMobileList: true. - Process: set
forms.mobile = "processes/<key>/<key>-mobile.xml".
Full entry creation is handled by vitruvio-criar-painel / vitruvio-criar-processo.
Step 5 — Report
Tell the user:
- File created/updated and variant (panel/process).
- Which variables/queries/bridges were declared, and that only declared data is available on the device — anything else must be added as an Autoload variable, QueryDataSource, or Bridge.
- Reminder: client scripts are modern JS (Promises); Bridge bodies are server-side Rhino ES5.
- For processes: each
<form formKey>must match anactiviti:formKeyin the BPMN. - Suggest reading
docs/components/mobile/and an example (~/.local/share/vitruvio-platform/examples/processes/*/form_web_mobile.xml) for richer screens.