wapform_flutter
WapForm for Flutter Community Edition: an open-source Flutter port of a Lazarus/FPC WAP form and report engine for Web and mobile.
With WapForm, platform limits disappear.
Why this exists
Cross-platform was never really the expensive part — the real cost is having to rebuild everything from scratch every time you switch platforms.
Even with Lazarus/LCL's desktop components fully ported to Flutter —
familiar data-aware widgets like TDataSet, TDBGrid,
TDBNavigator, the field and event model — developers still have to
build every screen in Dart, bind every dataset, and hand-write every
event handler and piece of business logic. At bottom, that's still
rewriting the same application on a different platform.
What WapForm actually solves isn't the widgets — it's how the application gets defined in the first place.
WML is a plain-text, declarative application-definition language. Datasources, fields, lookups, events, forms, and reports are all defined by description — not bound to Dart, Flutter, or any specific platform.
Which means the real development flow becomes:
Define once, land anywhere.
WapForm is the single application definition; the generator expands it into target-platform code; the runtime supplies the components and behavior the code actually needs to run.
And this repository is Flutter's runtime.
It isn't another Flutter UI framework — it's the Lazarus/LCL component model's counterpart implementation on Flutter. Every line of Dart WapForm's generator produces targets this API, so the same WML that originally ran on Windows can land on Flutter without changing its definition at all.
That's why "write ten lines of WML" gets you a running Flutter screen.
Because what's actually being reused isn't the code — it's the application's definition itself.
Architecture
WapForm for Flutter is three layers, and (per WapForm's licensing) the bottom two are what live in this repository:
| Layer | What it is | Where |
|---|---|---|
| WapForm | Declarative capabilities on top of layer 2: WML definitions, the generator, master-detail structures, banded group reports. | this repo's wapform_*.dart files |
| Lazarus for Flutter | Direct line-by-line translation of Lazarus/LCL's component model to Dart — TDataSet · TSQLQuery · TDBGrid · TDBNavigator · fields · events. |
this repo's lazarus_*.dart files |
| Lazarus / LCL | The original Object Pascal desktop component library this port is translated from. | upstream, not part of this repo |
Each layer only depends on the one below it. lazarus_*.dart has no
knowledge that WapForm exists — it's a standalone, general-purpose
port of the Lazarus component model, usable on its own for any
Dart/Flutter project that wants data-aware widgets. wapform_*.dart
is what turns that generic component layer into the specific thing a
WML file's generated output expects to find: an expression evaluator,
a tag engine, a lookup-box widget, and a banded report builder — the
runtime that the generator's emitted Dart code calls into.
All three layers are open source. The two in this repo are LGPL-2.1 with the FPC static-linking exception ("Modified LGPL") — see License below. WapForm separately sells a WML-to-Dart generator that targets exactly this open-source API — code it generates is ordinary Dart calling ordinary classes from this repo, no proprietary runtime involved. You can build against this repo entirely by hand instead; either way, what you end up depending on and maintaining is the code here.
What WapForm Actually Is: Define Once, Land Anywhere
Porting Lazarus/LCL's desktop component model to Flutter gets you
familiar data-aware components — TDataSet, TDBGrid,
TDBNavigator, the field and event model — giving WapForm's existing
application model somewhere to run inside Flutter.
But a component library alone doesn't make app development faster. Developers still have to build every screen, dataset, field, and event handler in Dart, by hand, one line at a time.
What WapForm actually solves is a level above that: how an application should be defined.
WML is WapForm's platform-agnostic, declarative application-definition language. Datasources, fields, lookups, events, forms, and reports are all described in WML — not bound directly to Dart, Flutter, or any other specific language.
Which means:
WapForm is the application's source of truth.
The generator's job is to expand that definition into target-platform code; the runtime and component library are the landing surface where that code actually runs.
That's this repository's role: the port of Lazarus/LCL's components and behavior onto Flutter, serving as the stable target platform that WapForm's generator produces Dart code against.
What AI Actually Contributes: Not Freeform Generation
Handing an LLM the Lazarus/FCL-DB source and asking it to translate file after file into Dart, even if it produces a large volume of code, doesn't mean it can reliably complete a port of this scale.
The real difficulty is behavioral consistency, API correspondence, the data model, the event model, and long-term maintainability.
WapForm takes a different approach:
WML Definition → Generator → Stable Runtime/API → Target Code
AI isn't asked to freely "write an app." It's doing large-scale, mechanical, repeatable code expansion against a WML definition that's already unambiguous, targeting a target API that's already stable.
This port of WapForm for Flutter was completed in about two months of AI-assisted development, against an initial estimate of six to twelve months for a hand port of the same scope. The real difference isn't just that AI writes code faster — it's:
Establish the definition first, then let AI generate against it.
So those two months aren't just a demonstration of AI development speed — they're a real-world validation of this "definition → generation → runtime → execution" engineering pattern.
On COBOL: Not a Language Converter, and Not Starting From Zero
Plenty of AI tools today advertise converting COBOL into Java, C#, or other modern languages. But:
Successfully converting code isn't the same as completing a system port.
What actually makes COBOL enterprise systems hard is usually the data structures, transaction flows, field semantics, batch jobs, reports, exception handling, and behavior shaped by the original runtime — all accumulated over years. So a plain COBOL → Java conversion easily turns into COBOL → AI → Java → Compile → Debug → Test → revise again, and even once it compiles, confirming the converted system actually behaves the same as the original often still takes substantial cross-platform debugging and verification.
WapForm takes a different direction: it doesn't treat "the original source code" as the only source of truth — it lifts the application up to WML, a platform-agnostic definition layer. A future WapForm for COBOL wouldn't just "generate COBOL" — an existing COBOL system's logic, data, fields, flows, events, and reports could convert into WML (Existing COBOL → WapForm for COBOL → WML), and that path isn't one-way: the same WML could land back on COBOL itself (COBOL → WapForm for COBOL → WML → COBOL), making WapForm for COBOL not just an exit ramp for legacy systems but a way to regenerate and verify one — confirming the converted behavior matches the original, or producing a cleaner COBOL version aligned with the current runtime. Once inside WML, the application is no longer tied to COBOL, and the same definition can go through the Flutter generator and runtime that already exist: Existing COBOL → WML → Flutter, not a fresh COBOL → Flutter translation each time. If WML already fully describes the application, generating Flutter from it shouldn't require re-understanding COBOL or a large-scale cross-language translation — just adjustments for whatever platform differences remain. That's what actually matters about WML as an intermediate definition layer.
WapForm for COBOL doesn't exist yet — but Flutter has already completed the first full validation of the engineering chain Definition → Generator → Runtime → Target Code, so building the runtime, component model, and generator COBOL needs later doesn't mean reinventing all of WapForm from scratch. Later platforms may even land in the same or a shorter cycle than Flutter did, because the hardest part — the method, the architecture, the generation pipeline — already exists:
The first platform already established and validated the hardest engineering method; later platforms can build on that foundation.
Which means the future can look like:
COBOL → WML → COBOL
COBOL → WML → Flutter
and
WML → other platforms: MicroPython, Node.js
WML → other domains: AI, EDA
Different platforms and domains no longer each have to re-define the same application from scratch.
What Is WapForm Actually Building?
So WapForm's core isn't really
WapForm for Flutter
nor
WapForm for COBOL
— it's a layer above both:
Let the application be defined first, then realized on whichever platform.
WapForm is the center. AI is the accelerator. The runtime is the landing surface. The platform is just the final realized form.
That's also the biggest difference between WapForm and typical AI code-conversion tools:
AI Code Conversion
Source Code → AI → Another Source Code
WapForm
Application Definition → Target Runtime → Target Code
The former moves code between languages. The latter frees the application's definition from any platform at all.
So WapForm for Flutter isn't just one Flutter port — it's the first full proof of WapForm's cross-platform architecture.
And WapForm for COBOL won't just be the next "language converter." It can become the bridge that lets an existing COBOL system enter WML, and from there land on Flutter or any other platform.
One Definition. Any Platform. Every Domain.
From WML to this API
To make the two layers above concrete: this is the same WapForm declarative pattern — documented for the desktop product — landing on the classes this repo provides.
A WML screen declares a dataset and binds a form to it:
<card id="P" title="Employee master">
<dbquery name="em">
<![[select * from employees]]>
<field fieldname="emp_no" displaylabel="Employee ID"/>
<field fieldname="emp_name" displaylabel="Name"/>
<field fieldname="dept" displaylabel="Department"/>
<field fieldname="hired" displaylabel="Start date" type="date"/>
</dbquery>
<datasource dataset="em">
<navigator/>
<fieldset>
Employee ID: <input field="emp_no" size="8"/>
Name: <input field="emp_name" size="20"/>
Department: <input field="dept" size="12"/>
Start date: <input field="hired" type="date" size="12"/>
</fieldset>
</datasource>
</card>
Nothing here says how to build a cursor, wire up Save/Delete, or
validate a date field — <dbquery> declares the query and its fields
with an inline SQL block, <datasource> binds a form to them, and
<navigator/> is the entire CRUD button bar in one line. WapForm for
Flutter exports that declaration
into Dart calling exactly the classes in this repo:
// <dbquery id> → TSQLQuery; fields created in <field> order
late final TSQLQuery _em = dbquery("em", r"select * from employees", define: (ds) {
_key(ds, "emp_no", "employee_id");
_fld(ds, "emp_name", "name");
_fld(ds, "dept", "department");
_fld(ds, "hired", "start_date", dateOnly: true);
});
// <datasource> + <navigator> → TDataSource + TDBNavigator
final _dsEm = TDataSource()..dataSet = _em;
TDBNavigator(dataSource: _dsEm), // First/Prev/Next/Last/New/Delete/Save/Cancel
// <input field="..."> → TDBEdit bound to the dataset
TDBEdit(dataSource: _dsEm, dataField: "employee_id"),
TDBEdit(dataSource: _dsEm, dataField: "name"),
A master-detail <datasource> nested inside another (linked by
masterfields) becomes a second TSQLQuery whose beforeOpen/events
filter by the parent's key; a <dbgrid><item> inside it becomes a
TDBGrid with matching TColumns; an <input lookup=> field becomes
a WapLookupBox wired to the lookup dataset — each declarative WML tag
has exactly one landing spot in this repo's API, which is what lets
the generator's output be entirely mechanical. A shipment order and
its line items, for example:
<datasource dataset="sh">
<navigator/>
<fieldset>
Shipment No.: <input field="sno" size="14" readonly="true"/>
Customer: <input field="cno" size="14" lookup="cu;cno;cname"/>
</fieldset>
<datasource dataset="sn" masterfields="sno">
<navigator/>
<dbgrid height="200">
<item field="pno" size="14" lookup="pa;pno;des"/>
<item field="qty" size="8"/>
<item field="price" size="9"/>
</dbgrid>
</datasource>
</datasource>
// <datasource dataset="sh"> — the master, a plain navigable list
late final TSQLQuery _sh = dbquery("sh", r"select * from sh");
final _dsSh = TDataSource()..dataSet = _sh;
// <datasource dataset="sn" masterfields="sno"> nested inside it — the
// detail. "masterfields" means: re-open _sn, filtered by the master's
// current "sno", every time the master's cursor moves.
late final TSQLQuery _sn = dbquery("sn", r"select * from sn where sno=:sno");
final _dsSn = TDataSource()..dataSet = _sn;
@override
void initState() {
super.initState();
_sh.afterScroll = (ds) {
_sn.close();
_sn.params[0].asString = ds.fieldByName("sno").asString;
_sn.open();
};
}
// <dbgrid><item field="pno" lookup="pa;pno;des"/>...</dbgrid>
// → one TColumn per <item>, added to a TDBGridColumns; a lookup=
// field is handled by the grid's lookupResolver, not the column itself
TColumn _column(TDBGridColumns cols, String field, String caption, num size) {
final c = cols.add();
c.fieldName = field;
c.width = (size * 8).round();
c.title.caption = caption;
return c;
}
late final TDBGridColumns _snCols = () {
final c = TDBGridColumns();
_column(c, "pno", "Item No.", 14);
_column(c, "qty", "Quantity", 8);
_column(c, "price", "Unit Price", 9);
return c;
}();
TDBGrid(
dataSource: _dsSn,
columns: _snCols,
lookupResolver: (field) => field.toLowerCase() != "pno"
? null
: TDBGridLookupSpec(rows: _paRows, colWidths: const [160]),
),
The report engine works the same way. WML's <group change="...">
groups rows and accumulates subtotals with setvar, letting the
framework handle pagination and reprinted headers on page breaks —
that's wapform_report.dart's WapReport, where emitRow() plays
the same role, whether the row came from <group> in WML or a
hand-written report subclass. A statement grouped by customer, with a
running subtotal:
<report dataset="sh">
<group change="sh.cno">
<setvar name="AMOUNT_SUM" value="0"/>
<page>
<p>$(sh.cname) — statement</p>
<table>
<tr><th>Ship Date</th><th>Shipment No.</th><th>Amount</th></tr>
<group>
<setvar name="AMOUNT_SUM" value="AMOUNT_SUM+sh.amount"/>
<tr>
<td>$(sh.sdate)</td>
<td>$(sh.sno)</td>
<td>$(sh.amount)</td>
</tr>
</group>
</table>
<p align="right">Subtotal: $(AMOUNT_SUM)</p>
</page>
</group>
</report>
// <group change="sh.cno"> — a new group (and a fresh AMOUNT_SUM) every
// time sh.cno changes; a plain <group> with no change= is the RECORD
// block, one call per row
class ShStatement extends WapReport {
@override
void parseBlock(String id) {
switch (id) {
case 'G1_PREFIX':
setvar("AMOUNT_SUM", "0");
emitRow(expandText(
r'''<p>$(sh.cname) — statement</p><table>'''
r'''<tr><th>Ship Date</th><th>Shipment No.</th><th>Amount</th></tr>'''));
break;
case 'RECORD':
setvar("AMOUNT_SUM", "AMOUNT_SUM+sh.amount");
emitRow(expandText(
r'''<tr><td>$(sh.sdate)</td><td>$(sh.sno)</td><td>$(sh.amount)</td></tr>'''));
break;
case 'G1_SUFFIX':
emitRow(expandText(
r'''</table><p align="right">Subtotal: $(AMOUNT_SUM)</p>'''));
break;
}
}
}
emitRow() counts lines and breaks the page automatically once it's
full, reprinting the header on the next one — callers never track row
counts or page breaks themselves, whether the row came from <group>
in WML (compiled into parseBlock()'s switch, as above) or a
hand-written report subclass calling emitRow() directly.
File reference
wapform_flutter.dart is the barrel file — import 'package:wapform_flutter/wapform_flutter.dart' pulls in every file
below. Every file it exports is also importable on its own (e.g.
package:wapform_flutter/lazarus_sqldb.dart), so a project that only
needs, say, TDBGrid isn't forced to pull in the report engine too.
That's also why these files live flat in lib/ instead of behind
lib/src/ — the usual Dart convention of hiding implementation behind
src/ would block per-file imports, and each file here is meant to be
a usable unit on its own.
Lazarus layer (lazarus_*.dart)
Direct translations of the corresponding FPC RTL / FCL-DB / LCL units. Each file's header names the exact upstream source and marks every place the translation deviates from it.
lazarus_db.dart — db.pas · dataset.inc · fields.inc ·
datasource.inc · database.inc · dsparams.inc · dbconst.pas
The data-access foundation everything else builds on: TDataSet ·
the full TField hierarchy · TFieldDefs · TDataLink/TDataSource ·
TParams · and the buffer-window mechanism that makes scrollable,
editable datasets possible.
lazarus_sqldb.dart — sqldb.pp
SQL-backed datasets: TSQLConnection · TSQLQuery · TSQLTransaction.
Also includes TWapSQLConnection — original bridging code (not a
translation) that implements the driver interface over HTTP, since a
browser can't open a raw TCP connection to a database.
lazarus_dbctrls.dart — dbctrls.pp
Single-field data-bound controls: TFieldDataLink (the data-binding
core every bound widget uses) · TDBEdit · TDBMemo · TDBRadioGroup ·
TDBNavigator.
lazarus_dbgrids.dart — dbgrids.pas
The editable data grid: TComponentDataLink · TColumn/TDBGridColumns ·
and a TDBGrid widget with Excel-style cell selection, keyboard
navigation, and inline editing.
lazarus_grids.dart — grids.pas (column-model subset)
The column data model (TGridColumn · TGridColumnTitle ·
TGridColumns) that lazarus_dbgrids.dart builds on. Grid
painting/selection itself isn't translated — LCL's is Canvas-based,
with no Flutter equivalent.
lazarus_stdctrls.dart — stdctrls.pp
Standard controls: TEdit · TMemo · TLabel · TComboBox ·
TListBox · TCheckBox · TRadioButton · TButton · TGroupBox ·
TScrollBar.
lazarus_extctrls.dart — extctrls.pp
Extended controls: TRadioGroup · TCheckGroup · TPanel · TBevel ·
TShape · TImage · TTimer/TIdleTimer · TLabeledEdit ·
TFlowPanel · TSplitter.
WapForm layer (wapform_*.dart)
Original code (not translated from Lazarus/FPC) that depends on the layer above and gives WML's generated output somewhere to run.
wapform_expression.dart
The WML expression engine (MyParser/WapEvaluator): the
lexer/parser/evaluator behind every $(...) expression in a WML
file, plus the full built-in function library (nearly 400 functions —
see wapform.com for the complete reference).
Data-layer agnostic — no dependency on the Lazarus layer.
wapform_lazarus.dart
The WML tag engine wired to the Lazarus data layer:
expression()/condition()/setvar()/invoke() · DataSetRegistry ·
and DbQuery — this is what a generated screen's <onevent> and
<dbquery> tags compile down to calling.
wapform_lookup_box.dart
WapLookupBox — the unified lookup-dropdown widget used both as a
standalone field and as a TDBGrid cell editor. What <input lookup=>/<dbtable> lookups compile to.
wapform_filter.dart
WapFilter — the search-filter bar widget. What <dbfilter result=""><item field= size=/>...<onevent type="onfilter"> compiles
to. Field labels come from the dataset's displaylabel automatically;
the SQL template is taken verbatim from the onfilter handler's
<dbquery> text, which already contains the literal $R token this
widget's search logic substitutes (equals / range / starts-with /
ends-with / contains, based on what the user types).
wapform_report.dart
The banded report engine (WapReport · WapPage): pagination, nested
group headers/footers, and HTML/PDF/print output. What <group change>/<report> compile to.
wapform_report_style.dart
The shared CSS for report screen-preview and print output, kept in one
place so every report looks consistent.
wapform_colors.dart
WapColors — resolves the row/zebra-stripe CSS classes reports and
grids use, loaded from a shared asset with built-in fallbacks.
lib/src/report_web*.dart
Platform-conditional support for wapform_report.dart: the
dart:html/dart:ui_web code needed for in-page report preview on
Flutter Web, isolated behind a conditional import so the package still
compiles on mobile/desktop.
Example pages (example/lib/)
The 12 .dart files in example/lib/ are generator-output pages
produced by TWapFlutterWriter (the tool that translates WML
form-description files into Flutter code) — the Flutter front end of
one sales management system, sharing a single WapProductionDB
connection and this repo's own package:wapform_flutter/ utility
library (dataset wrapping, TDBGrid, field mapping, lookup fields,
etc. — the raw generator output imports these as wap/xxx.dart
relative paths; they've been switched to proper package imports here).
Each file is one screen (a form card) in the system, and internally
they're all structured the same way: dbquery() to register a
dataset, _fld/_key/_lkp to build the field mapping, TDataSource
to bind the screen, and Search/Clear/Print/Close button logic. What
differs page to page is the underlying table and business purpose.
Login (wapform_login.dart) and a fully data-driven main menu
(wapform_menu.dart, its contents read entirely from the mnu/
login/users tables) sit in front of all 12 — see
example/README.md for how that's wired
together:
Each
.wmlfile sitting in the same directory is the WapForm source program that produced the paired.dartfile next to it, included specifically so you can compare the two and see the translation process — every.dartis exactly what its.wmlcompiles down to.
app001.dart— System parameters: company name, address, tax ID, tax rate, closing period, and similar settings (tablesys).app002.dart— Product/inventory master: item number, barcode, category, brand, cost, and multiple price tiers, with nested sub-forms (subcategory, group) and print/report output (main tablepa;veis a lookup source table for brand, not the main table).app003.dart— Brand master: brand code, name, address, tax ID, contact info (tableve).app004.dart— Customer master: customer code, name, address, tax ID, delivery zone, contact info, email/password, plus print output (tablecu).app005.dart— Employee records: national ID, birthday, hometown, gender, marital status, military service, education, start/leave dates, department/title, phone (tableem).app006.dart— The largest file: combined shipment/purchase order workflow, including customer and employee masters, a price-editing sub-form, and both Shipment Order and Purchase Order report output, plus an external link viaurl_launcher(tablescu,em, and the shipment tables).app007.dart— Customer lookup/print page: simpler, mainly for browsing the customer list and printing a report (tablecu).app012.dart— A grouped report: the Accounts Receivable Statement (App012ReportP1 extends WapReport), grouped by customer with a running subtotal per shipment — this is the actual page behind the grouped-report example in this README's "From WML to this API" section. It also queriessys, but only for the report header's company name/tax rate — that's supporting data, not what the page is for.app023.dart— Payment receipt maintenance: shipment number, customer, tax rate, amount (incl. tax), amount paid, tied to tax and company settings (tablessys, the shipment tables,cu,fm).app037.dart— Shipper master: shipper code and name (tablefm).app901.dart— User account management: login credentials, password, email, last login time, auto-login, department (tableusers).app902.dart— Menu and permissions management: menu items (title, client/server target, active flag) and each user's per-menu read/write permissions (tablesmnu,login,users).
Taken together, these pages form the data-maintenance layer of a typical sales management system: master data (customers, brands, employees, shippers, products), transaction documents (shipment orders, purchase orders, payment receipts), a report (the AR statement), and system administration (parameters, user accounts, menu permissions) — all produced by the same WML→Flutter translation engine.
function.wml / function.dart
Sitting alongside the 12 business pages, function.wml/
function.dart isn't one of them — it's a 363-case self-test report
for the WML expression engine (wapform_expression.dart's built-in
function library), cross-checking each documented example expression
against its documented expected value. It's what actually drove
several real fixes in wapform_expression.dart, including two
byte-order bugs in Color2Hex/Hex2Color and a sign-inversion bug in
ROUND/ROUNDTO. It has a real, permanent mnu row ("Function
Self-Test") — any account with a matching login permission row can
open it through the normal login → menu path, the same as any
business page; main.dart also wires it to a debug-only button as a
faster shortcut while developing. See
example/README.md for the details.
For the complete picture of WapForm's architecture — every WML tag, every function, and how the pieces fit together — see wapform_manual, the complete technical manual downloadable from wapform.com.
Install
dependencies:
wapform_flutter:
git:
url: https://github.com/wapform/wapform_flutter.git
(or path: ../wapform_flutter while developing locally, or via pub.flutter-io.cn
once published).
import 'package:wapform_flutter/wapform_flutter.dart';
Individual files can also be imported directly, e.g.
package:wapform_flutter/lazarus_sqldb.dart, if you only need part of
the package.
Want to see it running? example/ has 12 real
generator-output pages, each paired with its source .wml file — see
INSTALL.md for a full step-by-step setup (Flutter SDK,
the database, the gateway server) or
example/README.md for the short version.
Platform Support & Web-First Architecture
WapForm's core focus and implementation are centered on the Web platform, with Android now verified as well — the full example app (the 12 business pages, login, the data-driven menu, and the report/PDF pipeline) has been fully tested against real Android hardware, not just the report path in isolation. iOS, desktop, and further platform polish are still future work.
- A lean, Web-native implementation: outside of Flutter Web
builds, there's no compile-time dependency on
dart:html(seelib/src/report_web.dartfor the conditional import that makes this work), keeping the core architecture clean and extensible. - Consistent behavior: data access,
TDBGrid,WapLookupBox, the WML expression engine, and on-screen report preview viaflutter_htmlall behave highly consistently on the Web platform. - Native HTML reports and printing: on Flutter Web,
WapPage's print button opens the report's HTML directly in a new tab and calls the browser's nativewindow.print()smoothly — fully covering Web-side print and export needs. On Android, the same print button instead goes through a native WebView +PrintDocumentAdapterpipeline (flutter_native_html_to_pdf) to produce a real PDF — the fix that replaced an earlierPrinting.convertHtml()-based approach after it turned out to hang indefinitely on some real devices (a known upstream bug, not something specific to this package).
License
LGPL-2.1 with the FPC static-linking exception ("Modified LGPL") — see LICENSE, COPYING.LGPL.txt, and COPYING.modifiedLGPL.txt. In short: you can use this package in closed-source apps without restriction; only modifications to the package's own source files need to be published under the same license if you redistribute them.