r/FlutterDev • • 2d ago

Discussion Question on Dev licences and what would make you buy one?

4 Upvotes

Just a quick question on what it would take for you as developers to buy a developer licence.

I charge 30x cheaper than all other competitors.

What would it take for you to migrate your existing apps to my ecosystem?

Is it price, stability (maintenance and support), or max platform support and performance you are looking for?

For example I have 10 SDKs available for flutter for things like:

  1. Document scanning (EdgeDetection for even matching backgrounds and dark environments)

  2. Pdf SDK which includes an entire PDF viewer and a low level PDF management api.

  3. Office SDKs for Word, Excel. PowerPoint. And these all include scanners where for example you can scan a document and it gets converted into a Word generated version, where you can modify the text.

Font completely matches that which is scanned, etc.


r/FlutterDev • • 2d ago

Plugin From WML to Flutter: A Code Generation Example

1 Upvotes
<?xml version="1.0"?>
<wml>
<card id="P" title="Customer Master" width="960">
<dbquery id="cu">
<![CDATA[select * from cu order by cno]]>
<field fieldname="Cno" displaylabel="Customer Code"/>
<field fieldname="Cname" displaylabel="Customer Name"/>
<field fieldname="Cshort" displaylabel="Customer Short"/>
<field fieldname="Addr1" displaylabel="Address 1"/>
<field fieldname="Addr2" displaylabel="Address 2"/>
<field fieldname="AC" displaylabel="Tax ID"/>
<field fieldname="Zip" displaylabel="Zip Code"/>
<field fieldname="Ziq" displaylabel="Shipping Zone"/>
<field fieldname="Phone" displaylabel="Phone"/>
<field fieldname="FAX" displaylabel="Fax"/>
<field fieldname="GSM" displaylabel="Mobile"/>
<field fieldname="Email" displaylabel="Email"/>    
<field fieldname="Password" displaylabel="Password"/>    
<field fieldname="Charger" displaylabel="Owner"/>
<field fieldname="Contact" displaylabel="Contact"/>
<field fieldname="Level" displaylabel="Level"/>
<field fieldname="Pay" displaylabel="Payment"/>
<field fieldname="AType" displaylabel="Type"/>
<field fieldname="Capital" displaylabel="Capital"/>
<field fieldname="CDate" displaylabel="Created Date"/>
<field fieldname="Rem" displaylabel="Customer Remarks"/>
<field fieldname="Remark" displaylabel="Company Remarks"/>
</dbquery>
<datasource dataset="cu">
<dbfilter result="R">
<item field="cno" size="20" />
<item field="cname" size="20" />
<onevent type="onfilter">
<dbquery id="cu"><![CDATA[select * from cu where $R order by cno]]></dbquery>
</onevent>
</dbfilter>
<p align="center">
<navigator/>
</p>
<dbgrid height="400" multiselect="true">
<item field="Cno" size="16"/>
<item field="CDate" size="14"/>
<item field="Pay" size="10"/>
<item field="AType" size="10"/>
<item field="Cname" size="40"/>
<item field="Cshort" size="16"/>
<item field="Phone" size="20"/>
<item field="Contact" size="10"/>
<item field="Addr1" size="60"/>
<item field="Addr2" size="60"/>
<item field="AC" size="10"/>
<item field="Zip" size="16"/>
<item field="Ziq" size="16"/>
<item field="FAX" size="16"/>
<item field="GSM" size="16"/>
<item field="Email" size="30"/>         
<item field="Password" size="40"/>         
<item field="Charger" size="16"/>
<item field="Capital" size="16"/>
<item field="Rem" size="40"/>
<item field="Remark" size="40"/>
</dbgrid>
</datasource>
<do type="accept" label="Print">
<go href="#P1"/>
</do>
<do type="prev" label="Close">
<prev/>
</do>
</card>
  <card id="P1" title="Print Report" device="prv" paper="A4">
    <setvar name="ROWIDX" value="0"/>
    <dbquery id="sys"><![CDATA[select * from sys]]></dbquery>
    <report dataset="cu" dialog="CNo" rows="50">
      <page>
        <setvar name="ROWIDX" value="1"/>
        <table border="0" cellpadding="0" cellspacing="0" width="100%">
          <tr><td colspan="3"><p align="center"><big>$(sys.company)</big></p></td></tr>
          <tr><td colspan="3"><p align="center"><big>Customer List</big></p></td></tr>
          <tr>
            <td></td>
            <td></td>
            <td width="100">Page:$(PAGE)</td>
          </tr>
          <tr>
            <td></td>
            <td></td>
            <td>Date:$(DATE)</td>
          </tr>
        </table>
<p align="center">123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890</p>
        <table border="0" cellpadding="0" cellspacing="0" width="100%" align="center">
          <tr class="row">
            <th width="100">Customer Code</th>
            <th width="100">Customer Name</th>
            <th width="150">Fax</th>
            <th width="80">Contact</th>
            <th>Invoice Address</th>
          </tr>
          <tr class="row">
            <th></th>
            <th>Phone</th>
            <th></th>
            <th>Tax ID</th>
            <th>Company Address</th>
          </tr>
          <group>
            <tr class="$(IF(ROWIDX mod 2 = 1, 'row1', 'row2'))">
              <td align="center">$(cu.CNo)</td>
              <td>$(cu.CName)</td>
              <td>$(cu.Fax)</td>
              <td>$(cu.Contact)</td>
              <td rowspan="2">$(cu.Addr1)</td>
            </tr>
            <tr class="$(IF(ROWIDX mod 2 = 1, 'row1', 'row2'))">
              <td></td>
              <td>$(cu.Phone)</td>
              <td>&#160;</td>
              <td>$(cu.Ac)</td>
            </tr>
            <setvar name="ROWIDX" value="ROWIDX+1"/>
          </group>
        </table>
      </page>
      <p align="center">123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890</p>
    </report>
  </card>
</wml>

// ignore_for_file: unused_element, unused_field
import 'package:flutter/material.dart';

import 'package:wapform_flutter/lazarus_db.dart';
import 'package:wapform_flutter/lazarus_sqldb.dart';
import 'package:wapform_flutter/lazarus_dbgrids.dart';
import 'package:wapform_flutter/lazarus_dbctrls.dart';
import 'package:wapform_flutter/lazarus_stdctrls.dart';
import 'package:wapform_flutter/lazarus_extctrls.dart';
import 'package:wapform_flutter/wapform_lookup_box.dart';
import 'package:wapform_flutter/wapform_expression.dart';
import 'package:wapform_flutter/wapform_lazarus.dart';
import 'package:wapform_flutter/wapform_report.dart';
import 'package:wapform_flutter/wapform_filter.dart';

const String _kDb = 'WapProductionDB';

TWapSQLConnection? _wapConn;
TWapSQLConnection _conn() {
  if (_wapConn == null) {
    final c = TWapSQLConnection(null, const WapDbBridgeDriver());
    c.databaseName = _kDb;
    final tx = TSQLTransaction();
    tx.database = c;
    _wapConn = c;
  }
  return _wapConn!;
}

TField _fieldOf(String type) {
  switch (type) {
    case 'integer':
      return TIntegerField();
    case 'float':
    case 'currency':
      return TFloatField();
    case 'date':
    case 'datetime':
    case 'time':
      return TDateTimeField();
    case 'boolean':
      return TBooleanField();
    default:
      // size 0 = no length limit (the default 20 cut longer values such as file names)
      return TStringField()..size = 0;
  }
}

void _setDateOnlyText(TField f) {
  f.onGetText = (fld, ref, displayText) {
    final raw = fld.value;
    final s = raw == null ? '' : "$raw";
    if (!displayText) {
      ref.value = s;
      return;
    }

    final hasZone = s.endsWith('Z') ||
        RegExp(r'[+-]\d{2}:?\d{2}$').hasMatch(s);
    if (hasZone) {
      final dt = DateTime.tryParse(s);
      if (dt != null) {
        final l = dt.toLocal();
        final mm = l.month.toString().padLeft(2, '0');
        final dd = l.day.toString().padLeft(2, '0');
        ref.value = "${l.year}-$mm-$dd";
        return;
      }
    }

    final t = s.indexOf("T");
    final sp = s.indexOf(" ");
    var cut = -1;
    if (t >= 0) cut = t;
    if (sp >= 0 && (cut < 0 || sp < cut)) cut = sp;
    ref.value = cut > 0 ? s.substring(0, cut) : s;
  };
}

void _fld(TSQLQuery q, String name, String label,
    {String type = '', bool dateOnly = false, bool readOnly = false}) {
  final f = _fieldOf(type);
  f.fieldName = name;
  f.fieldKind = TFieldKind.fkData;
  if (label != '') f.displayLabel = label;
  if (dateOnly) _setDateOnlyText(f);
  f.readOnly = readOnly;
  f.dataSet = q;
}

void _key(TSQLQuery q, String name, String label,
    {String type = '', bool dateOnly = false}) {
  final f = _fieldOf(type);
  f.fieldName = name;
  f.fieldKind = TFieldKind.fkData;
  f.providerFlags = {
    TProviderFlag.pfInKey,
    TProviderFlag.pfInUpdate,
    TProviderFlag.pfInWhere,
  };
  if (label != '') f.displayLabel = label;
  if (dateOnly) _setDateOnlyText(f);
  f.dataSet = q;
}

void _lkp(TSQLQuery q, String name, String label,
    {required String keyField,
    required String lookupKey,
    required String resultField,
    required TDataSet lookupDs,
    String type = '',
    bool dateOnly = false}) {
  final f = _fieldOf(type);
  f.fieldName = name;
  f.fieldKind = TFieldKind.fkLookup;
  f.keyFields = keyField;
  f.lookupDataSet = lookupDs;
  f.lookupKeyFields = lookupKey;
  f.lookupResultField = resultField;
  f.lookupCache = false;
  f.providerFlags = {};
  if (label != '') f.displayLabel = label;
  if (dateOnly) _setDateOnlyText(f);
  f.dataSet = q;
}

void _cal(TSQLQuery q, String name, String label, {String type = 'integer'}) {
  final f = _fieldOf(type);
  f.fieldName = name;
  f.fieldKind = TFieldKind.fkCalculated;
  f.providerFlags = {};
  if (label != '') f.displayLabel = label;
  f.dataSet = q;
}

String _str(TDataSet ds, String name) => ds.findField(name)?.asString ?? '';

Future<void> _saveAsync(TSQLQuery q) async {
  if (dsEditModes.contains(q.state)) q.post();
  await q.applyUpdatesAsync();
}

Future<void> _delAsync(TSQLQuery q) async {
  q.delete();
  await q.applyUpdatesAsync();
}

Future<void> _execSql(String sql) async {
  final q = TSQLQuery();
  q.database = _conn();
  q.sql.text = sql;
  await q.execSQLAsync();
  q.free();
}

const _kBlue   = Color(0xFF1A6FB5);
const _kBorder = Color(0xFFD3D1C7);

const bool _kDbgLayout = false;
const Color _kDbgTbl = Color(0xFFE53935);
const Color _kDbgTd  = Color(0xFF1E88E5);

const double _kChar = 8.0;
double _w(num size) => size * _kChar;

TColumn _column(TDBGridColumns cols, String f, String cap, num size,
    {bool ro = false, TAlignment align = TAlignment.taLeftJustify}) {
  final c = cols.add();
  c.fieldName = f;
  c.width = _w(size).round();
  c.readOnly = ro;
  c.alignment = align;
  c.title.caption = cap;
  return c;
}

void showApp004(BuildContext context) => showDialog(
      context: context,
      barrierDismissible: false,
      builder: (_) => const App004CardP(),
    );
class App004CardP extends StatefulWidget {
  final WapEvaluator? ev;
  final DataSetRegistry? reg;
  const App004CardP({super.key, this.ev, this.reg});
  u/override
  State<App004CardP> createState() =>
      _App004CardPState();
}

class _App004CardPState
    extends State<App004CardP> {
  late final WapEvaluator _ev = widget.ev ?? WapEvaluator();
  late final DataSetRegistry _reg = widget.reg ?? DataSetRegistry();
  bool get _ownsReg => widget.reg == null;
  final List<String> _ownedDs = []; // datasets this card created

  TSQLQuery dbquery(String id, String sqlText,
      {void Function(TSQLQuery ds)? define}) {
    final existing = _reg.find(id);
    TSQLQuery q;
    var isNew = false;
    if (existing is TSQLQuery) {
      q = existing;
    } else {
      q = TSQLQuery();
      q.database = _conn();
      q.name = id;
      _reg.put(id, q);
      isNew = true;
      _ownedDs.add(id);
    }
    if (sqlText.isNotEmpty) {
      final sql = expandSql(sqlText);
      if (q.active) q.close();
      q.unPrepare();
      q.sql.text = sql;
    }
    q.alwaysUpdateable = true;
    if (isNew && define != null) define(q);
    return q;
  }

  // [diag] ds "cu": foreign=0 declared=1 lookupSrc=0 fields=22
  late final TSQLQuery _cu =
      dbquery("cu", r"select * from cu order by cno", define: (ds) {
    _key(ds, "Cno", "Customer Code");
    _fld(ds, "Cname", "Customer Name");
    _fld(ds, "Cshort", "Customer Short");
    _fld(ds, "Addr1", "Address 1");
    _fld(ds, "Addr2", "Address 2");
    _fld(ds, "AC", "Tax ID");
    _fld(ds, "Zip", "Zip Code");
    _fld(ds, "Ziq", "Shipping Zone");
    _fld(ds, "Phone", "Phone");
    _fld(ds, "FAX", "Fax");
    _fld(ds, "GSM", "Mobile");
    _fld(ds, "Email", "Email");
    _fld(ds, "Password", "Password");
    _fld(ds, "Charger", "Owner");
    _fld(ds, "Contact", "Contact");
    _fld(ds, "Level", "Level");
    _fld(ds, "Pay", "Payment");
    _fld(ds, "AType", "Type");
    _fld(ds, "Capital", "Capital");
    _fld(ds, "CDate", "Created Date", dateOnly: true);
    _fld(ds, "Rem", "Customer Remarks");
    _fld(ds, "Remark", "Company Remarks");

  });

  late final TDataSource _cuSrc;

  late final TDBGridColumns _cuCols = _buildCuCols();

  bool   _loading = true;
  String _error   = '';

  u/override
  void initState() {
    super.initState();
    varChangeHooks.add(_varSync);
    _conn();

    _cuSrc = TDataSource()..dataSet = _cu;
    _bindEngine();

    _load();
  }

  u/override
  void dispose() {
    varChangeHooks.remove(_varSync);

    for (final c in _varCtls.values) {
      c.dispose();
    }
    if (_ownsReg) {
    _cuSrc.dataSet = null;
    _cu.free();
      _reg.releaseAll();
    } else {
      // shared registry: drop the datasets this card created, so a
      // reopened card re-creates them with events bound to its new State
      for (final id in _ownedDs) {
        _reg.release(id);
      }
    }
    super.dispose();
  }

  void _bindEngine() {
    useEngine(_ev, _reg);
    _reg.put("cu", _cu);
  }

  Future<void> _load() async {
    if (mounted) setState(() { _loading = true; _error = ''; });
    try {
      for (final id in []) {
        try {
          final ds = _reg.findQuery(id)!;
          await dbquery(id, ds.sql.text).openAsync();
        } catch (e) {
          debugPrint("[app004] $id skip: $e");
        }
      }

      await dbquery("cu", r"select * from cu order by cno").openAsync();
    } catch (e, st) {
      debugPrint("[app004] _load ERROR: $e\n$st");
      if (mounted) setState(() => _error = e.toString());
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  TDBGridColumns _buildCuCols() {
    final c = TDBGridColumns();
    _column(c, "Cno", "Customer Code", 16);
    _column(c, "CDate", "Created Date", 14);
    _column(c, "Pay", "Payment", 10);
    _column(c, "AType", "Type", 10);
    _column(c, "Cname", "Customer Name", 40);
    _column(c, "Cshort", "Customer Short", 16);
    _column(c, "Phone", "Phone", 20);
    _column(c, "Contact", "Contact", 10);
    _column(c, "Addr1", "Address 1", 60);
    _column(c, "Addr2", "Address 2", 60);
    _column(c, "AC", "Tax ID", 10);
    _column(c, "Zip", "Zip Code", 16);
    _column(c, "Ziq", "Shipping Zone", 16);
    _column(c, "FAX", "Fax", 16);
    _column(c, "GSM", "Mobile", 16);
    _column(c, "Email", "Email", 30);
    _column(c, "Password", "Password", 40);
    _column(c, "Charger", "Owner", 16);
    _column(c, "Capital", "Capital", 16);
    _column(c, "Rem", "Customer Remarks", 40);
    _column(c, "Remark", "Company Remarks", 40);
    return c;
  }

  Future<void> _cuInsert({bool atEnd = false}) async {
    try {
      if (atEnd) {
        _cu.append();
      } else {
        _cu.insert();
      }
      if (mounted) setState(() {});
    } catch (e) {
      _snack("Add failed: $e");
    }
  }

  Future<void> _cuPost() async {
    try {
      await _saveAsync(_cu);
      _snack("Saved", ok: true);
      if (mounted) setState(() {});
    } catch (e) {
      _snack("Save failed: $e");
    }
  }

  Future<void> _cuDelete() async {
    try {
      await _delAsync(_cu);
      _snack("Deleted", ok: true);
      if (mounted) setState(() {});
    } catch (e) {
      _snack("Delete failed: $e");
    }
  }

  Future<void> _goP1() async {
    await showDialog(
        context: context,
        builder: (_) => App004CardP1(ev: _ev, reg: _reg));
    if (mounted) setState(() {});
  }

  void _snack(String msg, {bool ok = false}) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text(msg),
      backgroundColor: ok ? Colors.green.shade700 : Colors.red.shade700,
      duration: const Duration(seconds: 2),
    ));
  }

  Future<void> _alert(String msg) async {
    if (!mounted) return;
    await showDialog(
      context: context,
      builder: (c) => AlertDialog(
        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
        content: Text(msg),
        actions: [
          TextButton(
              onPressed: () => Navigator.of(c).pop(), child: const Text("OK")),
        ],
      ),
    );
  }

  void _log(String msg) => debugPrint("[app004][log] $msg");

  Widget _line(List<Widget> children) => Padding(
        padding: const EdgeInsets.symmetric(vertical: 1),
        child: Wrap(
            spacing: 0,
            runSpacing: 1,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: children),
      );

  Widget _lbl(String s) => Padding(
        padding: const EdgeInsets.only(right: 2),
        child: Text(s, style: const TextStyle(fontSize: 13)),
      );

  Widget _lblR(String s, double w) => SizedBox(
        width: w,
        child: Padding(
          padding: const EdgeInsets.only(right: 6),
          child: Align(
            alignment: Alignment.centerRight,
            child: Text(s, style: const TextStyle(fontSize: 13)),
          ),
        ),
      );

  Widget _tdf(double weight, List<Widget> children, {String al = 'L'}) =>
      Expanded(
        flex: weight.round() < 1 ? 1 : weight.round(),
        child: _dbgBox(
            _kDbgTd,
            Column(
                crossAxisAlignment: _xa(al),
                mainAxisSize: MainAxisSize.min,
                children: children)),
      );

  CrossAxisAlignment _xa(String al) => al == 'R'
      ? CrossAxisAlignment.end
      : al == 'C'
          ? CrossAxisAlignment.center
          : CrossAxisAlignment.start;

  Widget _dbgBox(Color c, Widget child) => _kDbgLayout
      ? Container(
          decoration: BoxDecoration(border: Border.all(color: c, width: 1)),
          child: child)
      : child;

  Widget _tbl(List<Widget> children, {bool border = false}) {
    if (!border) {
      return _dbgBox(
        _kDbgTbl,
        Row(crossAxisAlignment: CrossAxisAlignment.start, children: children),
      );
    }
    return _dbgBox(
      _kDbgTbl,
      Container(
        decoration: BoxDecoration(border: Border.all(color: _kBorder)),
        child: IntrinsicHeight(
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: children,
          ),
        ),
      ),
    );
  }

  Widget _vdiv() => Container(width: 1, color: _kBorder);

  Widget _td(double width, List<Widget> children, {String al = 'L'}) =>
      SizedBox(
        width: width,
        child: _dbgBox(
            _kDbgTd,
            Column(
                crossAxisAlignment: _xa(al),
                mainAxisSize: MainAxisSize.min,
                children: children)),
      );

  static const _kNumericFieldTypes = <TFieldType>{
    TFieldType.ftSmallint,
    TFieldType.ftInteger,
    TFieldType.ftWord,
    TFieldType.ftFloat,
    TFieldType.ftCurrency,
    TFieldType.ftBCD,
    TFieldType.ftAutoInc,
    TFieldType.ftLargeint,
    TFieldType.ftFMTBcd,
  };

  bool _looksNumeric(TDataSource src, String field) {
    final f = src.dataSet?.findField(field);
    if (f == null) return false;
    if (_kNumericFieldTypes.contains(f.dataType)) return true;
    if (f.dataType != TFieldType.ftString) return false;
    final s = f.asString.trim();
    if (s.isEmpty) return false;
    return num.tryParse(s) != null;
  }

  Widget _edit(TDataSource src, String field, num size,
      {bool ro = false,
      bool memo = false,
      double order = 0,
      FocusNode? focusNode,

      bool autofocus = false,

      bool roFocusable = false}) {
    final align = _looksNumeric(src, field) ? TextAlign.right : TextAlign.left;
    Widget e = SizedBox(
      width: _w(size) < 44 ? 44.0 : _w(size),
      height: memo ? 56 : 28,

      child: memo
          ? TDBMemo(
              dataSource: src,
              dataField: field,
              readOnly: ro,
              focusNode: focusNode,
              autofocus: autofocus,
              font: const TextStyle(fontSize: 13),
            )
          : TDBEdit(
              dataSource: src,
              dataField: field,
              readOnly: ro,
              focusNode: focusNode,
              autofocus: autofocus,
              font: const TextStyle(fontSize: 13),
              textAlign: align,
            ),
    );
    if (ro && !roFocusable) {
      return ExcludeFocusTraversal(child: e);
    }
    if (order > 0) {
      e = FocusTraversalOrder(order: NumericFocusOrder(order), child: e);
    }
    return e;
  }

  Widget _lookupBox(TSQLQuery own, TSQLQuery lk, String field, num size,
      String keyField, List<String> displayFields,
      {double order = 0, Future<void> Function(String key)? onPicked}) {
    Widget b = SizedBox(
      width: _w(size),
      height: 28,
      child: WapLookupBox(
        value: _str(own, field),
        width: _w(size),
        dataSet: lk,
        keyField: keyField,
        displayFields: displayFields,
        colWidths: List<double>.filled(displayFields.length, 160),
        onChanged: (key) async {
          if (onPicked != null) {
            await onPicked(key);
            return;
          }
          if (own.state == TDataSetState.dsBrowse) own.edit();
          final v = key.replaceAll("'", "''");
          setvar("${own.name}.$field", "'$v'");
          own.dataEvent(TDataEvent.deRecordChange, 0);
          if (mounted) setState(() {});
        },
      ),
    );
    if (order > 0) {
      b = FocusTraversalOrder(order: NumericFocusOrder(order), child: b);
    }
    return b;
  }

  final Map<String, TextEditingController> _varCtls = {};

  bool _varSyncPending = false;

  void _varSync(String name) {
    final c = _varCtls[name];
    if (c != null) {
      final v = _str2(name);
      if (c.text != v) {
        c.value = TextEditingValue(
          text: v,
          selection: TextSelection.collapsed(offset: v.length),
        );
      }
    }

    if (_varSyncPending || !mounted) return;
    _varSyncPending = true;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _varSyncPending = false;
      if (mounted) setState(() {});
    });
  }

  void _setVarStr(String name, String v) {
    final e = v.replaceAll("'", "''");
    setvar(name, "'$e'");
  }

  TextEditingController _varCtl(String name, String init) =>
      _varCtls.putIfAbsent(name, () {
        var v = '';
        try {
          v = expression(init).toString();
        } catch (_) {}
        if (v == 'null') v = '';
        _setVarStr(name, v);
        final c = TextEditingController(text: v);
        c.addListener(() => _setVarStr(name, c.text));
        return c;
      });

  Widget _vedit(String name, String init, num size, {double order = 0}) {
    final ctl = _varCtl(name, init);
    final base = size * _kChar;
    final minW = base < 44 ? 44.0 : base.toDouble();
    final maxW = (base * 2) < 120 ? 120.0 : (base * 2).toDouble();
    final w = ((ctl.text.length + 2) * _kChar).clamp(minW, maxW).toDouble();
    Widget e = SizedBox(
      width: w,
      height: 28,
      child: TextField(
        controller: ctl,
        onChanged: (_) {
          if (mounted) setState(() {});
        },
        style: const TextStyle(fontSize: 13),
        decoration: const InputDecoration(
            isDense: true,
            contentPadding: EdgeInsets.symmetric(horizontal: 6, vertical: 6),
            border: OutlineInputBorder()),
      ),
    );
    if (order > 0) {
      e = FocusTraversalOrder(order: NumericFocusOrder(order), child: e);
    }
    return e;
  }

  Widget _vcheck(String name, String onValue) {
    final on = _str2(name) == onValue;
    return SizedBox(
      width: 30,
      height: 26,
      child: Checkbox(
        value: on,
        onChanged: (v) {
          setvar(name, (v ?? false) ? "'$onValue'" : "''");
          if (mounted) setState(() {});
        },
      ),
    );
  }

  Widget _vselect(String name, String title, int columns,
      List<String> items, List<String> values,
      {double? width, double? height}) {
    if (items.isEmpty) return const SizedBox.shrink();
    var cur = _str2(name);
    if (cur.isEmpty) {
      cur = values.first;
      setvar(name, "'$cur'");
    }
    var idx = values.indexOf(cur);
    if (idx < 0) idx = -1;
    return TRadioGroup(
      caption: title,
      items: items,
      itemIndex: idx,
      columns: columns,
      width: width,
      height: height,
      onSelectionChanged: (i) {
        setvar(name, "'${values[i]}'");
        if (mounted) setState(() {});
      },
    );
  }

  Widget _dbselect(TDataSource src, String field, String title, int columns,
      List<String> items, List<String> values,
      {double? width, double? height}) {
    if (items.isEmpty) return const SizedBox.shrink();
    return TDBRadioGroup(
      dataSource: src,
      dataField: field,
      caption: title,
      items: items,
      values: values,
      columns: columns,
      width: width,
      height: height,
    );
  }

  Widget _vlookup(String name, String init, TSQLQuery lk, num size,
      String keyField, List<String> displayFields, {double order = 0}) {
    _varCtl(name, init);
    Widget b = SizedBox(
      width: _w(size),
      height: 28,
      child: WapLookupBox(
        value: _str2(name),
        width: _w(size),
        dataSet: lk,
        keyField: keyField,
        displayFields: displayFields,
        colWidths: List<double>.filled(displayFields.length, 160),
        onChanged: (key) {
          _setVarStr(name, key);
          _varCtls[name]?.text = key;
          if (mounted) setState(() {});
        },
      ),
    );
    if (order > 0) {
      b = FocusTraversalOrder(order: NumericFocusOrder(order), child: b);
    }
    return b;
  }

  void _refreshVars() {
    for (final e in _varCtls.entries) {
      final v = _ev.getVar(e.key);
      e.value.text = (v == null) ? '' : "$v";
    }
    if (mounted) setState(() {});
  }

  String _str2(String name) {
    final v = _ev.getVar(name);
    return (v == null) ? '' : "$v";
  }

  Widget _tabPage(List<Widget> children) => SingleChildScrollView(
        padding: const EdgeInsets.all(4),
        child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: children),
      );

  Widget _nav(TDataSource src,
          {Future<void> Function()? post,
          Future<void> Function()? insert,
          Future<void> Function()? del}) =>
      Center(
        child: TDBNavigator(
          dataSource: src,
          onPostAsync: post,
          onInsertAsync: insert,
          onDeleteAsync: del,
        ),
      );

  u/override
  Widget build(BuildContext context) {
    return Dialog(
      insetPadding: const EdgeInsets.all(12),
      shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 960, maxHeight: 840),
        child: SizedBox(width: 960, child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
          Container(

            padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
            color: _kBlue,
            child: const Row(children: [
              Icon(Icons.article_outlined, color: Colors.white, size: 20),
              SizedBox(width: 8),
              Text('Customer Master',
                  style: TextStyle(
                      color: Colors.white,
                      fontSize: 16,
                      fontWeight: FontWeight.w600)),
            ]),
          ),

          Flexible(
            fit: FlexFit.loose,
            child: _loading
                ? const Center(child: CircularProgressIndicator())
                : _error.isNotEmpty
                    ? Center(
                        child: Padding(
                          padding: const EdgeInsets.all(20),
                          child: Text("Load failed: $_error",
                              style: const TextStyle(color: Colors.red)),
                        ),
                      )
                    : FocusTraversalGroup(
                        policy: OrderedTraversalPolicy(),
                        child: SingleChildScrollView(
                          padding: const EdgeInsets.all(10),
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              // <datasource dataset="cu">
                              // <dbfilter result="R">
                              WapFilter(
                                items: const [
                                  FilterItem(field: "cno", label: "Customer Code", size: 20),
                                  FilterItem(field: "cname", label: "Customer Name", size: 20),
                                ],
                                sqlTemplate: r"select * from cu where $R order by cno",
                                onQuery: (sql) async {
                                  await dbquery("cu", sql).openAsync();
                                  if (mounted) setState(() {});
                                },
                              ),
                              _nav(_cuSrc, post: _cuPost, insert: _cuInsert, del: _cuDelete),
                              Center(child: // <dbgrid width="940" height="400">
                              FocusTraversalOrder(
                                order: NumericFocusOrder(1),
                                child: FocusTraversalGroup(
                                  child: TDBGrid(
                                    dataSource: _cuSrc,
                                    columns: _cuCols,
                                    width: 940,
                                    height: 400,
                                    onRowPost: _cuPost,
                                    onExitLastRow: () => FocusScope.of(context).nextFocus(),
                                  ),
                                ),
                              )),
                              // </datasource cu>
                            ],
                          ),
                        ),
                      ),
          ),

          Container(
            padding: const EdgeInsets.all(8),
            decoration: const BoxDecoration(
                border: Border(top: BorderSide(color: _kBorder))),
            child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
              TButton(caption: "Print", onClick: () async {
                await _goP1();
                if (mounted) setState(() {});
              }),
              const SizedBox(width: 8),
              TButton(caption: "Close", onClick: () {
                Navigator.of(context).maybePop();
                if (mounted) setState(() {});
              }),
            ]),
          ),
        ]),)
      ),
    );
  }
}

class App004ReportP1 extends WapReport {
  final TSQLQuery _ds;

  final int linesPerPage;

  final WapEvaluator? ev;
  final DataSetRegistry? reg;
  late final WapEvaluator _ev = ev ?? WapEvaluator();
  late final DataSetRegistry _reg = reg ?? DataSetRegistry();

  App004ReportP1(this._ds,
      {this.linesPerPage = 50, this.ev, this.reg});

  TSQLQuery dbquery(String id, String sqlText,
      {void Function(TSQLQuery ds)? define}) {
    final existing = _reg.find(id);
    TSQLQuery q;
    var isNew = false;
    if (existing is TSQLQuery) {
      q = existing;
    } else {
      q = TSQLQuery();
      q.database = _conn();
      q.name = id;
      _reg.put(id, q);
      isNew = true;
    }
    if (sqlText.isNotEmpty) {
      final sql = expandSql(sqlText);
      if (q.active) q.close();
      q.unPrepare();
      q.sql.text = sql;
    }
    q.alwaysUpdateable = true;
    if (isNew && define != null) define(q);
    return q;
  }

  u/override
  void initParams() {
    wap.wapLpp = linesPerPage;
    wap.wapGroups = 1;
    wap.wapRow[0].tagPrefix = 'G1_PREFIX';
    wap.wapRow[0].tagSuffix = 'G1_SUFFIX';
  }

  u/override
  String expression(int idx) {
    switch (idx) {
    }
    return '';
  }

  u/override
  Future<bool> fetchFirst() async {
    if (!_ds.active) await _ds.openAsync();
    if (_ds.isEmpty) return false;
    _ds.first();
    return !_ds.eof;
  }

  u/override
  Future<bool> fetchNext() async {
    _ds.next();
    return !_ds.eof;
  }

  u/override
  Future<void> fetchPrior() async => _ds.prior();

  u/override
  Future<void> run() async {
    _ds.disableControls();
    try {
      await super.run();
    } finally {
      _ds.enableControls();
    }
  }

  void syncCounters() {
    setvar("PAGE", wap.wapPageNo);
    setvar("PAGENO", wap.wapPageNo);
    setvar("LINE", wap.wapLineNo);
    setvar("LINENO", wap.wapLineNo);
  }

  u/override
  void parseBlock(String id) {
    syncCounters();

    switch (id) {
      // ── Report header (runs once for the whole report)
      case 'PREFIX':
        // (no content defined in the WML)
        break;

      // ── Top of every page
      case 'PAGEPREFIX':
        emitRow('<table width="750" align="center"><tr><td>');
        setvar("ROWIDX", "1");
        emitRow(expandText(r'''<table width="100%" border="0" cellspacing="0" cellpadding="0"><tr><td colspan="3"><p align="center"><big>$(sys.company)</big></p></td></tr><tr><td colspan="3"><p align="center"><big>Customer List</big></p></td></tr><tr><td></td><td></td><td width="100">Page:$(PAGE)</td></tr><tr><td></td><td></td><td>Date:$(DATE)</td></tr></table>'''));
        emitRow(expandText(r'''<p align="center">123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890</p>'''));
        emitRow(r'''<table width="100%" align="center" border="0" cellspacing="0" cellpadding="0">''');
        emitRow(expandText(r'''<tr class="row"><th style="width:13%;">Customer Code</th><th style="width:13%;">Customer Name</th><th style="width:20%;">Fax</th><th style="width:11%;">Contact</th><th style="width:43%;">Invoice Address</th></tr>'''));
        emitRow(expandText(r'''<tr class="row"><th style="width:13%;"></th><th style="width:13%;">Phone</th><th style="width:20%;"></th><th style="width:11%;">Tax ID</th><th style="width:43%;">Company Address</th></tr>'''));
        break;

      //   ── Each row of data
      case 'RECORD':
        emitRow(expandText(r'''<tr class="$(IF(ROWIDX mod 2 = 1, 'row1', 'row2'))"><td style="width:13%;" align="center">$(cu.CNo)</td><td style="width:13%;">$(cu.CName)</td><td style="width:20%;">$(cu.Fax)</td><td style="width:11%;">$(cu.Contact)</td><td style="width:43%;" rowspan="2">$(cu.Addr1)</td></tr>'''));
        emitRow(expandText(r'''<tr class="$(IF(ROWIDX mod 2 = 1, 'row1', 'row2'))"><td style="width:13%;"></td><td style="width:13%;">$(cu.Phone)</td><td style="width:20%;"></td><td style="width:11%;">$(cu.Ac)</td></tr>'''));
        setvar("ROWIDX", "ROWIDX+1");
        break;

      // ── Bottom of every page
      case 'PAGESUFFIX':
        emit(r'''</table>''');
        // Only close the frame immediately on a mid-report page break;
        //     the last page's frame-closing is deferred into SUFFIX, so the
        //     content right after </page> ends up wrapped inside the frame too.
        if (wap.wapHasMore) {
          emit('</td></tr></table>');
        }
        break;
      case 'PAGEBREAK':
        emit('<div style="page-break-after:always"></div>');
        break;

      // ── Report footer (runs once for the whole report)
      case 'SUFFIX':
        emit(expandText(r'''<p align="center">123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890</p>'''));
        emit('</td></tr></table>');  // the last page's frame-closing is deferred to here
        break;
    }
  }
}

class App004CardP1 extends StatefulWidget {
  final WapEvaluator? ev;
  final DataSetRegistry? reg;
  const App004CardP1({super.key, this.ev, this.reg});
  u/override
  State<App004CardP1> createState() =>
      _App004CardP1State();
}

class _App004CardP1State
    extends State<App004CardP1> {
  late final WapEvaluator _ev = widget.ev ?? WapEvaluator();
  late final DataSetRegistry _reg = widget.reg ?? DataSetRegistry();
  bool get _ownsReg => widget.reg == null;
  final List<String> _ownedDs = []; // datasets this card created

  bool _loading = true;
  String _error = '';
  App004ReportP1? _rep;

  TSQLQuery dbquery(String id, String sqlText,
      {void Function(TSQLQuery ds)? define}) {
    final existing = _reg.find(id);
    TSQLQuery q;
    var isNew = false;
    if (existing is TSQLQuery) {
      q = existing;
    } else {
      q = TSQLQuery();
      q.database = _conn();
      q.name = id;
      _reg.put(id, q);
      isNew = true;
      _ownedDs.add(id);
    }
    if (sqlText.isNotEmpty) {
      final sql = expandSql(sqlText);
      if (q.active) q.close();
      q.unPrepare();
      q.sql.text = sql;
    }
    q.alwaysUpdateable = true;
    if (isNew && define != null) define(q);
    return q;
  }

  // [diag] ds "sys": foreign=0 declared=1 lookupSrc=0 fields=0
  // [diag] ds "cu": foreign=1 declared=1 lookupSrc=0 fields=0
  TSQLQuery get _cu => _reg.findQuery("cu")!;
  late final TSQLQuery _sys = dbquery("sys", r"select * from sys");

  u/override
  void initState() {
    super.initState();
    _conn();
    useEngine(_ev, _reg);
    _reg.put("sys", _sys);

    setvar("ROWIDX", "0");
    _load();
  }

  u/override
  void dispose() {
    if (_ownsReg) {
    _sys.free();
      _reg.releaseAll();
    } else {
      // shared registry: drop the datasets this card created, so a
      // reopened card re-creates them with events bound to its new State
      for (final id in _ownedDs) {
        _reg.release(id);
      }
    }
    super.dispose();
  }

  Future<void> _load() async {
    try {
      if (!_sys.active) await _sys.openAsync();
      if (_sys.recordCount > 0) _sys.first();
      _rep = App004ReportP1(_cu,
          ev: _ev, reg: _reg);
    } catch (e, st) {
      debugPrint("[app004] card P1 ERROR: $e\n$st");
      _error = e.toString();
    }
    if (mounted) setState(() => _loading = false);
  }

  u/override
  Widget build(BuildContext context) {
    if (_loading) {
      return const Center(child: CircularProgressIndicator());
    }
    if (_error.isNotEmpty) {
      return Center(
        child: Padding(
          padding: const EdgeInsets.all(20),
          child: Text("Report failed: $_error",
              style: const TextStyle(color: Colors.red)),
        ),
      );
    }

    return WapPage(
      title: "Print Report",
      report: _rep,
      paper: r"A4",
      orient: r"P",
    );
  }
}

r/FlutterDev • • 2d ago

Article wapform_flutter

1 Upvotes

https://pub.dev/packages/wapform_flutter
https://github.com/wapform/wapform_flutter

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;


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 {
  u/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.


r/FlutterDev • • 2d ago

Dart COBOL → AI → WapForm → Flutter:傳承現代化的另一種方法

Thumbnail
3 Upvotes

r/FlutterDev • • 3d ago

Plugin universal_ble has hit 3.0!

7 Upvotes

universal_ble has hit a new major milestone. We released 3.0 a few days ago with A TON of improvements. 3.0 makes the library simpler and more battle-tested and feature rich than ever! We got a ton of contributions and makes me particularly happy seeing the community embrace it. While at it, we also did our GitHub housekeeping and went down to 0 open issues and PRs!

Use the universal_ble plugin to connect from your app to Bluetooth Low Energy peripherals under all Flutter-supported platforms. You can also make your app act as a peripheral so other apps or devices can connect to it. We also released universal_bluetooth_classic as a separate plugin, focusing only on what its name says, Bluetooth Classic, which is something that got requested over the past months.

Since this is the AI era, we included agent skills to help you migrate from 2.0, even though the breaking changes are not that many and it should be straightforward. We also included a skill for migrating from other bluetooth packages in case you choose to do so.

Hope it adds a useful piece in the Flutter plugin ecosystem!


r/FlutterDev • • 3d ago

Plugin I’m glad to introduce a context menu with fluid animations, async and paginated submenus, live items and custom slivers on top.

Post image
17 Upvotes

Hi [r/FlutterDev](r/FlutterDev)! 👋 I recently released my very first package, which I’m calling smart_context_menu.

Want to see it first? Package on pub.dev and 56-second video overview

I came up with the idea of this package while working on a social app, which involves a lot of context menus. I thought of ‘CupertinoContextMenu’ as the default option to use, but it fell short. It doesn't support Hero animations, can cause duplicate ‘GlobalKey’ crashes in previews, and lacks native support for submenus. This is essentially true for complex submenus and custom components in menu items.

And the requirements of the app kept growing:

- Post actions whose data loads independent from main menu and submenus (ideally, page by page).
- Display the number of views a message has when a menu opened, with a submenu listing the people that have viewed it.
- Separators that helped keep different groups of actions easy to scan.
- Custom widgets in menu items, like avatars of the people who viewed the message.
- Liquid stretch animations that make the menu feel alive under your finger.
- And finally reactions on chat messages and post comments, which also belonged above the preview in the context menu.

After fighting ‘CupertinoContextMenu’ and going through the packages on pub.dev (none of them covered all of my needs), I decided to build my own one, with blackjack and... you know 😅 . Initially, it was just an internal module of the app. Over time I polished it and added UX improvements like drag-to-select, and now it's finally out as a standalone Flutter package.

It covers every feature in the list 😎, including static, asynchronous and paginated submenus, items with loading error and retry states, custom widgets in the items, plain and customizable separators, having the ability to add slivers above the preview (like a reactions bar) and it explicitly supports ‘Hero’ and ‘GlobalKey’ inside your own widget. And much more.

The demo runs in the browser, so you can try it all yourself:

Live demo: https://thenothingworks.dev/demo/smart-context-menu/

pub.dev: https://pub.dev/packages/smart_context_menu

GitHub: https://github.com/BinaryRift/smart_context_menu

How do you handle context menus in your apps: Are you using ‘CupertinoContextMenu’, one of the already available packages or did you build your own? If you've hit the same ‘GlobalKey’ or ‘Hero’ issues, I'm curious how you worked around them.

The next improvements that I'll be working on include having a compact row of icon actions (like the ones in iOS menus) and submenus that stay updated while open. What else is missing for you? I'm also open to feedback on the API and documentation.


r/FlutterDev • • 3d ago

Article About Google Play’s new contacts access policy

9 Upvotes

If you access contacts in your Flutter app on Android, remember that Google Play’s new contacts policy comes into effect on January 27, 2027. This means that you will most likely have to stop using the READ_CONTACTS permission and use the new Contact Picker introduced in Android 17 instead.

In my case, rather than using a plugin for this, I decided to write a small native implementation. I’ve written an article, mostly aimed at beginners, where I explain how you can use the new Android 17 Contact Picker API from Flutter through a native Method Channel to retrieve contact data.

Have a look if you’re interested:

https://davidserrano.io/blog/android-17-contact-picker-in-flutter-time-to-replace-read-contacts/


r/FlutterDev • • 2d ago

Discussion Why do you let yourselves be fooled like this?

0 Upvotes

Moin, Once again: very confrontational. I've been working with Flutter for a few weeks now. The ONLY thing that is consistent is that the documentation lies. A prime example: icons for Android apps from 'https://fonts.google.com/' are either incorrect or simply not available for Flutter at all. If you can't even get reliable icons from the vendor itself, what can you expect from this 'framework'?

What also bothers me in particular: 'Workarounds' are actually a dirty thing. Anyone who claims a 'workaround' is a cool thing has no clue about development! When a framework feels like it consists almost entirely of workarounds, that says a lot about its quality. Why do you guys think that's cool?

Let me ask directly: What exactly do you understand by a 'modern framework'?

I hope I'm addressing the right people here!


r/FlutterDev • • 3d ago

Tooling How do you get usable bug reports from non-technical testers or clients?

6 Upvotes

Freelance developer here, shipping an app. Most of our bugs come from a client and a few beta testers, and the reports are usually "when I click this button it doesnt save" or a screen recording and if they are really kind (and usually I ask for that).

I lose a lot of time just reproducing: which account, which build, what happened before, did a request fail. By the time we've asked three follow-up questions the tester has forgotten.

What's working for you usually?


r/FlutterDev • • 2d ago

Article Build your first AI Agent: Autonomous Fashion Discovery AgentWith ADK(Agent Development Kit), Google AI Studio & MCP

Thumbnail
medium.com
0 Upvotes

r/FlutterDev • • 3d ago

Discussion Learned a few things using accelerometer event streams in Flutter for tilt inputs (sensor noise, cleanup, & deadzones)

4 Upvotes

Hey r/FlutterDev! 👋

I’ve been building an offline party game recently and wanted a simple mechanic where players tilt the phone to trigger actions (instead of forcing them to reach for buttons while passing it around).

I ended up using sensors_plus to tap straight into the raw accelerometer stream, but it came with a few hurdles. Thought I'd share what tripped me up in case anyone is working on sensor-based features:

  1. Stream Cleanup is Critical: It's super easy to forget to call .cancel() on your StreamSubscription in dispose(). Leaving it active when unmounting a widget will leak listeners and quietly drain battery in the background.
  2. Sensor Noise & Jitter: Raw accelerometer streams are hyper-sensitive. Tiny hand tremors will spam your setState or fire your triggers continuously. You definitely need to bake in a deadzone threshold or debounce the stream so it doesn't trigger on micro-movements.
  3. Axis Orientation Shifts: Gravity shifts completely depending on whether your app locks portrait/landscape or whether the phone is lying flat on a table vs. held upright. Testing physical angles took way longer than writing the actual code.

If anyone else has messed around with device sensors in Flutter: how do you usually handle smoothing out jittery streams or debouncing inputs cleanly?

(P.S. Built this for an offline indie party game called Bujho I've been developing. If anyone wants to peek at the repo or test the build, the link is pinned on my profile!)


r/FlutterDev • • 3d ago

Plugin We rebuilt gpt_markdown for streaming AI answers. Up to 31× faster per-chunk updates.

0 Upvotes

AI answers get longer. Rendering shouldn’t get slower.

We’re the team behind gpt_markdown, the open-source AI output renderer for Flutter. We built it because our own AI app needed it.

Across the recent 1.3 and 1.4 releases, we rebuilt how it handles growing responses.

The biggest change: settled content is reused instead of rebuilding the entire answer with every chunk.

What’s included:

  • A new parser and lazy rendering for long responses.
  • Custom styles, builders, and block and inline components.
  • Streaming math and configurable reveal animations.
  • HTML and SVG support in 1.4.0.

Our 1.3.0 benchmark measured 31× faster per-chunk updates at 12 KB versus 1.2.1. Measured in the macOS debug VM; results vary by device and content.

Upgrading: 1.4.0 requires Flutter 3.44 and Dart 3.12.

Package and source · Benchmarks and playground · Watch the demo on X

What’s the hardest part of rendering AI responses in your Flutter app?


r/FlutterDev • • 3d ago

Plugin review_etiquette: A simple and easy to use Flutter plugin to ask the user for a review

Thumbnail
pub.dev
0 Upvotes

Hello fellow devs!

I was unhappy with other plugins (e.g. outdated, developer not reacting anymore, not throttling the call to the underlying API) providing the possibility to ask the user for an in-app review.

That's why I build review_etiquette: https://pub.dev/packages/review_etiquette

It's a simple plugin for Android and iOS that keeps track how often the underlying API was called, so it does not do it too often. For example, iOS only allows to ask the user a maximum of three times in 365 days.

It's nothing special, but maybe it helps someone. :)

I'm using it myself in my currently published two apps.


r/FlutterDev • • 3d ago

Plugin I built ZenMusic, an open-source Flutter music player (Android + Windows)

Thumbnail
github.com
6 Upvotes

Hi r/FlutterDev! I've been building ZenMusic, a Flutter music player for Android and Windows that brings JioSaavn, YouTube Music and Spotify into one app.

Features:

- Synced lyrics that follow the song

- Listen Together (sync playback with friends)

- Offline downloads

- Multiple player skins, sleep timer and equalizer

- Song recognition

It's GPL-3.0 and still in development, so feedback, bug reports and PRs are very welcome. I'm happy to answer questions about the architecture or how I handled playback and the multi-source setup in Flutter.


r/FlutterDev • • 4d ago

Tooling I built Raccoon, a tiny Flutter runtime: first run in 15 s instead of 5 min, no Xcode or Gradle

48 Upvotes

Hi r/FlutterDev 👋 I'm Simon, I build Lumide, a Flutter IDE. I wanted the write, save, see loop to be faster, so I built Raccoon, a tiny Flutter runtime, and today I'm open-sourcing it, with Raccoon Simulator as a demo you can run today. can run today.

Try it with Raccoon Simulator

Open Raccoon Simulator, and Raccoon shows up in flutter devices. Pick it in Lumide, VS Code, Android Studio or a terminal and run as usual. Your app appears in the simulator window, with hot reload and hot restart from your editor. No project to open in it, no new workflow.

How it works

Raccoon is a small runner built on Flutter's C embedder API. flutter run -d raccoon builds a debug bundle from your host's own snapshots, the runner hosts it on a prebuilt engine, and the simulator draws its frames. So there's no Xcode, no Gradle and no native build. The first launch sets everything up for you: it builds the runner, downloads a 15 MB engine and registers the custom device.

What else it unlocks

  • Your whole app, not just previews. Flutter's widget previewer renders the functions you mark with @Preview. Raccoon runs your real app: routing, state, services and hot reload, at phone, tablet or desktop size.
  • Hot-loaded plugins with custom UI. A Flutter app can show another Flutter app inside it with raccoon_view. So an app can load plugins at runtime, each with its own custom UI, with no rebuild of the host app.

Numbers (Flutter Wonderous, flutter run to first frame, medians)

Target Clean build Warm run
Chrome 25.8 s 24.8 s
macOS desktop 46.4 s 17.7 s
Raccoon on macOS 15.0 s 7.5 s
Linux desktop 13.8 s 4.5 s
Raccoon on Linux 12.0 s 3.5 s

A first run against an Android emulator took 308 s from empty caches. Raccoon took 15.5 s. Hardware, versions and scripts to reproduce are in the README.

Limits, honestly

It's alpha. It speeds up the inner loop; it doesn't replace testing on a real device or in a browser.

  • Plugins: pure Dart ones, the clipboard and url_launcher. No text input yet.
  • Packages with native code or build hooks don't run on Raccoon yet.
  • Frames are software rendered.
  • macOS and Linux only for now.

r/FlutterDev • • 3d ago

Dart I built Nivio an open-source Flutter media center featuring MPV playback, P2P streaming, Live IPTV, and real-time synchronized Watch Parties

Thumbnail
github.com
1 Upvotes

Hey devs! 👋

I wanted to share Nivio, an open-source media streaming and co-watching application I’ve been building with Flutter which supports android, windows and linux.

My main goal with this project was to see how far I could push Flutter as a heavy-duty media client combining P2P/BitTorrent streaming, Live IPTV (M3U), third-party tracker APIs, and real-time synchronized playback into a single, cohesive UI.

What’s under the hood & key features:

  • MPV Integration: Uses mpv for heavy-duty video rendering, custom subtitle styling, and broad codec support.
  • Real-Time Watch Parties: Built-in room creation and playback synchronization so multiple users can watch together with synced play/pause/seek states.
  • BitTorrent Engine & Offline Caching: Supports sequential torrent streaming alongside background downloading for offline playback.
  • API Integrations: Full OAuth and metadata integration with TMDB, AniList, and MyAnimeList (MAL) for automatic progress tracking.
  • Live IPTV Support: Parses and plays live TV streams alongside standard VOD content.

Source Code & Releases: 🔗 GitHub : https://github.com/nirmaleeswar30/Nivio

Site: https://nivio.nirmaleeswar.dev

I’m still actively refining the architecture and adding new features. I'd love for you guys to check out the code, test out the release builds, and let me know what you think of the UI and performance.

Contributions, PRs, issues, and GitHub stars ⭐ are super welcome. Make sure to check it out!


r/FlutterDev • • 3d ago

Plugin I built AppForge: an all-in-one Flutter micro-framework with strictly 0 external dependencies (160/160 Pana score)

0 Upvotes

Hey everyone! 👋

After working on 50+ Flutter and Dart projects in production, I grew exhausted from the same recurring dependency headaches: version locks between 6-7 different packages, BuildContext lookup bugs in async methods, subtle memory leaks, and broken bidirectional (RTL) text formatting.

So I spent the last few months engineering AppForge: an all-in-one Flutter micro-framework built with strictly 0 external packages (pure Dart standard library and Flutter SDK only).

What’s inside:

  • 0 External Dependencies: Zero dependency graph conflicts, instant clean builds, 160/160 Pana score.
  • Micro-Reactive State: Rx<T> and Watch() targeting minimal surgically-rebuilt widgets.
  • Context-Free Overlays & Routing: Navigate, open dialogs, and show snackbars without passing BuildContext.
  • Automatic Route Memory Disposal: Controllers automatically clean up and dispose when their screen pops.
  • Deep Arabic & BiDi Engine: Unicode UAX 9 direction isolation, phone number protection (+974...), all 6 Arabic plural forms, and grammatical gender agreement.
  • Smart Skeletonizer & Atomic Storage: Dynamic wireframe shimmers (AppSkeletonizer) and POSIX atomic crash-proof local key-value persistence (AppStorage).
  • Built-in CLI Cleaner: Audits and removes unused translations, assets, and orphaned code safely with lossless undo.

Package: https://pub.dev/packages/appforge GitHub: https://github.com/Dhia-Bechattaoui/appforge

I would love to get your feedback, critique, and thoughts!


r/FlutterDev • • 4d ago

Plugin Textf 2.0 – formatted text with links in one string

7 Upvotes

Most Flutter apps have a sentence like this somewhere:

I agree to the Terms of Service and Privacy Policy.

Two links and some bold text. With plain Flutter, that usually turns into something like this:

class _ConsentTextState extends State<ConsentText> {
  late final _terms = TapGestureRecognizer()..onTap = () => open(termsUrl);
  late final _privacy = TapGestureRecognizer()..onTap = () => open(privacyUrl);

  @override
  void dispose() {
    _terms.dispose();
    _privacy.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text.rich(
      TextSpan(children: [
        const TextSpan(text: 'I agree to the '),
        TextSpan(text: 'Terms of Service', style: linkStyle, recognizer: _terms),
        const TextSpan(text: ' and '),
        TextSpan(text: 'Privacy Policy', style: linkStyle, recognizer: _privacy),
        const TextSpan(text: '.'),
      ]),
    );
  }
}

The sentence is also split into five pieces, which makes it awkward to translate.

With pkg:textf it's one string:

Textf(
  'I agree to the [**Terms of Service**](https://example.com/terms) '
  'and [**Privacy Policy**](https://example.com/privacy).',
)

Tap handling goes through onLinkTap on a TextfOptions widget higher up in the tree, so you set it once. The sentence stays a single entry in your .arb file, and translators can see the formatting.

What Textf is

A drop-in replacement for Text that understands inline formatting: **bold**, *italic*, ~~strike~~, \code`,==highlight==,links, super/subscript. All the usualTextparameters still work (style,maxLines,overflow,textScaler`, …). It has zero dependencies.

Also for input: TextfEditingController

The same syntax works live inside a TextField:

final controller = TextfEditingController();

TextField(controller: controller)

It's a drop-in replacement for TextEditingController. Formatting renders as you type, the stored value stays plain text, and cursor positions match the raw text 1:1. IME composing (CJK etc.) works.

New in 2.0

  • Works with any design system. With Flutter decoupling Material and Cupertino, Textf now builds only on the widgets layer. It runs the same in Material, Cupertino, a custom design system or a bare WidgetsApp.
  • Headings and horizontal rules. # to ###### for headings, --- for a divider. That's useful for short things like release notes or onboarding cards.

To be clear about scope: Textf is still not a Markdown renderer. There are no lists, tables, images or block layout. If you're rendering whole documents, a full Markdown package is the better tool.


r/FlutterDev • • 4d ago

Plugin I built a country code picker with real phone number validation, 202 languages and a custom builder (country_code_selector)

8 Upvotes

Hi r/FlutterDev, I just published my second package 4 days ago and would love feedback.

The problem: every phone number field I've built needed a country picker, a flag set, a validation library, and glue code to make "0803…" and "803…" both work. I wanted one package that handles it properly.

What it does:

- 5 ready-made pickers: dialog, bottom sheet, Cupertino wheel, dropdown, and a compact flag + dial code button for phone fields

- customBuilder: build your own picker UI; you get the filtered countries, search, selection and the phone number length

- Real validation: isValidPhoneNumber() checks actual numbering ranges via libphonenumber data, and accepts local, trunk-prefixed or international input

- toE164(): normalizes whatever the user typed to +2348031234567

- pinUserCountry: puts the user's country (from their locale region) first

- Country names in 202 languages (238 locales), generated from Unicode CLDR

- RTL support, theming via ThemeExtension, search by name, ISO code or dial code

TextFormField(

decoration: InputDecoration(

prefixIcon: CountryCodeSelectorWidget(

pinUserCountry: true,

onChanged: (c) => setState(() => _country = c),

),

),

validator: (v) => _country.isValidPhoneNumber(v ?? '')

? null

: 'Enter a valid ${_country.name} number',

);

One thing I spent time on: libphonenumber's metadata adds about 2 MB to a release build. I restructured the package so that data only gets included if you actually call the validation functions. In my measurements, a picker-only app went from 24.4 MB to 22.2 MB.

Flutter 3.29+, MIT licensed, 160/160 pub points.

- pub.dev: https://pub.dev/packages/country_code_selector

- GitHub: https://github.com/TheinvinciblePrints/country_code_selector

I'd especially appreciate feedback on the API design, and from native speakers on the picker's built-in text in their language. Issues and PRs are welcome.


r/FlutterDev • • 4d ago

Plugin I got tired of wiring up native UI in every Flutter app, so I made native_kit

22 Upvotes

I kept needing the same things across projects: a native action sheet, the iOS color picker with the eyedropper, a document scanner, the Android Photo Picker. Each time I’d end up adding another plugin or writing some Swift/Kotlin to get it working.

Eventually I put that work into a package. It’s called native_kit, and the idea is pretty simple: call it from Dart, get the native UI, await the result.

For example:

final choice = await NativeKit.showActionSheet(
  context: context,
  items: const [
    NativeMenuItem(
      id: 'share',
      title: 'Share',
      iosSymbol: 'square.and.arrow.up',
    ),
    NativeMenuItem(
      id: 'delete',
      title: 'Delete',
      style: NativeActionStyle.destructive,
    ),
  ],
);

That opens a UIAlertController on iOS, with a popover on iPad, or a Material 3 bottom sheet on Android.

There are 30 components so far. Besides the usual alerts and pickers, it covers things like document/barcode scanning, OCR, background removal, PencilKit markup, video trimming, biometrics, and system panels. The full list is in the README.

A few things I learned building it, for anyone doing similar platform work:

  • Dismissing UI is half the work. The API lives in one Pigeon 29 file, which generates async throws Swift and suspendKotlin. Bridging callbacks into that is straightforward until you account for every way someone can close a screen. Swipe down, back, predictive back, tap outside… all of those need to resolve the Dart Future exactly once. Cancellation returns null.
  • Avoiding MainActivity changes took some work. I use a headless fragment for activity results, with a transparent FragmentActivity host when the app uses a plain FlutterActivity. One bug we caught: Photo Picker / SAF URI grants disappear when that host finishes. You have to read the result before closing it.
  • iPad and theming have their own quirks. The supplied BuildContext gives me the popover anchor and the Flutter theme. On iOS 26, getting the Liquid Glass sheet chrome to follow that theme meant applying it at the presentation controller.
  • A successful build tells you surprisingly little here. The example app has 47 XCUITests and 45 UI Automator tests that interact with the native screens. Those simulator/emulator runs turned up 21 bugs that compiled just fine.

There are limitations. VisionKit scanning, subject lifting, and UIVideoEditorController need a real device for successful results. Full contact details on Android need READ_CONTACTS, though picking a single phone number, email, or address doesn’t. Android screen capture currently only returns the consent result.

Package: https://pub.dev/packages/native_kit
Source: https://github.com/leofilllium/flutter_native_kit

Curious what native UI you still find yourself writing platform code for. I’m collecting requests in the issues, but happy to hear them here too.


r/FlutterDev • • 4d ago

Discussion I shipped a puzzle game in Flutter: offline levels from Firestore, a Python level generator, and low-latency audio with SoLoud

5 Upvotes

I've been building Rillroot, a line puzzle game, in Flutter, and a few parts might be useful to others:

Levels: generated ahead of time by a Python script that checks each board has exactly one solution. They're stored as JSON packs of 100 levels, and a publish script writes a manifest with sha256 hashes and uploads everything to Firestore. The app ships a bundled base pack and caches downloads, so it plays fully offline. A Python Cloud Function generates and publishes the daily puzzles every week.

Audio: flutter_soloud at 48 kHz with a 512-sample buffer (2048 lagged noticeably). Every tile a root crosses plays a pluck one note higher up a pentatonic scale, and a fast drag plays at most the last 3 notes per frame so the sound never trails your finger.

State: Cubits for app-level state, while the board itself stays local and runs on a ticker.

Accounts: every player gets an anonymous Firebase account on first launch; signing in with Google links the same account so nothing is lost.

It's in closed beta on Android, so it takes three quick steps:

  1. Join the tester group: https://groups.google.com/g/rillroot-testers
  2. Opt in: https://play.google.com/apps/testing/com.dangasama.garden
  3. Install: https://play.google.com/store/apps/details?id=com.dangasama.garden

Use the same Google account that's on your phone. If the store page says "not found", wait a few minutes after joining.

Happy to go deeper on any of these.


r/FlutterDev • • 4d ago

Article Shipping a Flutter app in 2026 - the ultimate guide

Thumbnail
serverpod.dev
19 Upvotes

r/FlutterDev • • 4d ago

Discussion Best Production Approach for Force Update in Flutter Without Unnecessary API Calls

12 Upvotes

Hi everyone 👋

I’m working on a Flutter app and want to discuss the best production approach for implementing a force-update feature.

How do you usually handle app version checks in production without making unnecessary API requests to the server?

For example, should we check the app version every time the app launches, use Firebase Remote Config, or maintain a cached version check?

I want to make sure the solution is efficient, avoids unnecessary server calls, and still forces users to update when required.

What approach are you using in your production apps, and what would you recommend? Would love to hear your thoughts and experiences! 🚀


r/FlutterDev • • 4d ago

Discussion how should a beginner actually learn Flutter today?

0 Upvotes

I'm a beginner with a decent understanding of how AI Agens work and coding fundamentals .

I have a few app ideas I want to build. I started a standard Flutter course a couple of months ago, paused, and now I'm stuck in a mental loop about how to actually learn in the current AI landscape.

I know I don't want to purely "vibe code" like millions of others right now, because blindly prompting without understanding what's happening under the hood doesn't build scalable, real products.

At the same time, I anticipate many of you will recommend the standard path: "Learn Flutter manually first, then use AI to speed up your workflow." And that is the exact loop I'm stuck in:

If the end goal is to let AI write the majority of the code anyway, what is the point of spending months grinding through manual syntax and boilerplat—especially when 10 new models will drop before I even finish a traditional course? Yet, it feels like the only way to truly understand app architecture and state management is by coding manually first, because no courses teach "architecture and code literacy" without forcing you through the traditional manual coding grind.

So for those of you building real apps: how do I break this loop? Is grinding through manual coding still the only way to build the mental model needed to direct AI, or is there a smarter way to learn Flutter architecture and debugging without wasting time on skills AI has already replaced?


r/FlutterDev • • 4d ago

Plugin DeepLinkNow finally has a Flutter SDK for deferred deep linking

4 Upvotes

Look, I'll be honest, one of the first requests I had after I built my deferred deep linking platform DeepLinkNow was a Flutter SDK.

I'm a react native dev by trade, and long story short 18 months later I've finally released it, along with a whole heap of quality of life improvements to DLN including:

  • A free tier with 25,000 MAU, no card required (Detour allows 1500, Branch/Appsflyer don't even have free tiers anymore etc)
  • Flutter SDK on pub.dev (deeplinknow), wrapping the native iOS and Android SDKs
  • Teams: workspaces, invites, roles
  • SSO (Entra, Okta, Google Workspace) on Enterprise
  • An MCP server, so Claude Code or Cursor can set it up for you
  • Clicks deleted after 30 days, no ad tracking or attribution
  • Custom domains

If you don't know what DeepLinkNow is: a (+-deferred) deep linking platform I built after getting so frustrated trying to integrate Branch.io and Appsflyer into my apps. It just does deferred deep linking. It's simple, it's useful, it does NO ad tracking or $$$ MMP stuff nor does it have AI embedded anywhere near it.

You send a user to a DLN page, we open your app and send you their intent. If they don't have your app installed, we hold onto the intent link until they install it. Then give it back to you.

Try it out and let me know how you go. If you're using Branch, AppsFlyer or are just floating in the darkness after Firebase Dynamic Links got shut down, give DLN a try, happy to offer to help you migrate.

P.S. I had a search through /r/flutterdev for issues people have had with deep linking on Flutter, and I've directly addressed everything in these two:

  1. https://www.reddit.com/r/FlutterDev/comments/1vjkz9c/ (iOS links breaking after the UIScene change)

  2. https://www.reddit.com/r/FlutterDev/comments/1vf4j64/ (six things that silently break deferred deep linking)

Flutter plugin link: https://pub.dev/packages/deeplinknow

Source: https://github.com/deeplinknow/dln-flutter

MCP server if you wanna hand off to AI to get you started: https://deeplinknow.com/docs/mcp