<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohammad Dallah</title>
    <description>The latest articles on DEV Community by Mohammad Dallah (@mdallah).</description>
    <link>https://dev.to/mdallah</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F878620%2Fb0447f9a-9825-4c05-9d21-8f6b3794d6ba.jpeg</url>
      <title>DEV Community: Mohammad Dallah</title>
      <link>https://dev.to/mdallah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mdallah"/>
    <language>en</language>
    <item>
      <title>A tiny template language for widget data, and the Android regex trap that bit me</title>
      <dc:creator>Mohammad Dallah</dc:creator>
      <pubDate>Tue, 10 Nov 2026 17:17:02 +0000</pubDate>
      <link>https://dev.to/mdallah/a-tiny-template-language-for-widget-data-and-the-android-regex-trap-that-bit-me-5ecb</link>
      <guid>https://dev.to/mdallah/a-tiny-template-language-for-widget-data-and-the-android-regex-trap-that-bit-me-5ecb</guid>
      <description>&lt;p&gt;An Android widget cannot render JSON. &lt;code&gt;RemoteViews&lt;/code&gt; takes a &lt;code&gt;TextView&lt;/code&gt;, and a &lt;code&gt;TextView&lt;/code&gt; takes a string. So somewhere between "here is a nested object" and "here is a 60-character line", you need a formatting step.&lt;/p&gt;

&lt;p&gt;Most widget apps solve this with a visual editor, or with code, or with Tasker. I wanted the third option nobody offers: &lt;strong&gt;a format string you can type on a phone keyboard in ten seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is the design of that little language, and the two mistakes I made building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint
&lt;/h2&gt;

&lt;p&gt;The format has to do five things, and nothing else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick a value out of a nested document: &lt;code&gt;{main.temp}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Project a list onto lines: &lt;code&gt;{stations.*.brand}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Repeat a row per item: &lt;code&gt;#each orders … #end&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Do one arithmetic operation, because APIs return cents: &lt;code&gt;{price / 1000}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Lay out multiple lines: &lt;code&gt;\n&lt;/code&gt;, &lt;code&gt;\t&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else — loops with accumulators, functions, conditionals over arrays — is out of scope. If your widget needs JavaScript, you don't want a widget, you want a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The grammar
&lt;/h2&gt;

&lt;p&gt;Eight forms, no operators to memorise:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{a.b.c}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dotted path, arrays index with a number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{a.*.b}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;project: map &lt;code&gt;b&lt;/code&gt; over every item of &lt;code&gt;a&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{a[0:5].b}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;slice before projecting (also &lt;code&gt;{a(3)}&lt;/code&gt; for the first three)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{price / 1000}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one operation: &lt;code&gt;+ - * /&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{?missing}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;render nothing instead of the placeholder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{missing ?: "n/a"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fallback text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`{status \&lt;/td&gt;
&lt;td&gt;1: On, *: Off}`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#each items where qty &amp;gt; 0&lt;/code&gt; … &lt;code&gt;#end&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;repeat a block per item&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A missing path renders &lt;code&gt;—&lt;/code&gt;, not an error. That one decision matters more than it looks: a sensor going &lt;code&gt;unavailable&lt;/code&gt; should not blank the whole tile, and an error string in a widget is worse than a dash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two decisions that paid off
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Resolve exactly once.&lt;/strong&gt; The scanner walks the format left to right and substitutes each fragment a single time. A value that itself looks like JSON is emitted verbatim, never re-parsed. This kills a whole class of bugs where user data gets interpreted as template syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No regex for blocks.&lt;/strong&gt; Matching &lt;code&gt;#each … #end&lt;/code&gt; with a regex works until it doesn't: nested blocks, &lt;code&gt;#elseif&lt;/code&gt;, and an &lt;code&gt;#end&lt;/code&gt; inside a string literal all break it. A single pass with a depth counter is shorter than the regex, and correct.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;MAX_DEPTH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StringBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"#each"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* read header, find matching #end, recurse per item */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"#if"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* pick a branch, recurse */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'{'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* find '}', resolve one expression, append */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The trap: braces are not always braces
&lt;/h2&gt;

&lt;p&gt;Here is the bug that cost me a release.&lt;/p&gt;

&lt;p&gt;Users write &lt;code&gt;{{field}}&lt;/code&gt;. They mean &lt;code&gt;{field}&lt;/code&gt;. Doubled braces are a de-facto standard from every other templating language, so this mistake is inevitable — and silently rendering &lt;code&gt;{21.4}&lt;/code&gt; is worse than failing loudly.&lt;/p&gt;

&lt;p&gt;So I added a check with a regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="no"&gt;TEMPLATE_MARK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"\\{\\{[^{}]*}}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiled. The unit tests passed. The app crashed on launch on a real device with &lt;code&gt;ExceptionInInitializerError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Android's &lt;code&gt;java.util.regex&lt;/code&gt; is backed by &lt;strong&gt;ICU&lt;/strong&gt;, not OpenJDK's engine, and ICU is stricter: a literal &lt;code&gt;}&lt;/code&gt; outside a character class that is not part of a valid interval quantifier is a syntax error. On the desktop JVM, &lt;code&gt;}}&lt;/code&gt; is tolerated. On the device, the class fails to initialise — and because it's a top-level &lt;code&gt;val&lt;/code&gt;, it fails when the class loads, not when the regex is used.&lt;/p&gt;

&lt;p&gt;The fix is one escape per brace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="no"&gt;TEMPLATE_MARK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"\\{\\{[^{}]*\\}\\}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson I now apply everywhere: &lt;strong&gt;a green JVM test suite does not mean your regexes compile on Android.&lt;/strong&gt; If a regex contains a literal brace, escape it. I added a test that scans every regex literal in the source tree and fails the build on an unescaped &lt;code&gt;{&lt;/code&gt; or &lt;code&gt;}&lt;/code&gt; — it is ten lines, and it will never let that class of bug through again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cap everything, because widgets are not views
&lt;/h2&gt;

&lt;p&gt;Two limits, both learned from &lt;code&gt;RemoteViews&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20 rows&lt;/strong&gt; per loop or projection. Beyond that, a &lt;code&gt;... (+N more)&lt;/code&gt; line. A 400-item array will happily crash the launcher, and nobody reads row 90.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2000 characters&lt;/strong&gt; of output. A widget is four lines tall.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rendering is not the place to be generous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;Yes, for one reason: the format string &lt;em&gt;is&lt;/em&gt; the documentation. When someone shares a tracker, they share the source URL and that string. You can read a tile's definition in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{attributes.friendly_name}\n{state}{attributes.unit_of_measurement}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No AST to export, no editor state, no version skew.&lt;/p&gt;

&lt;p&gt;The honest downside: non-developers look at &lt;code&gt;{a.*.b}&lt;/code&gt; and stop. The fix is not more syntax — it is presets that hide it. That is the next thing I am building.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F84nlcvke4anwqtku4ads.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F84nlcvke4anwqtku4ads.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;If you want to see the result: I ship it as an Android app called &lt;a href="https://play.google.com/store/apps/details?id=com.p48.zync" rel="noopener noreferrer"&gt;Zync&lt;/a&gt; — that link is the only promotion in this post, the syntax above is the actual subject.&lt;/p&gt;

&lt;p&gt;The full grammar reference is at &lt;a href="https://zync.dallah.dev/docs" rel="noopener noreferrer"&gt;zync.dallah.dev/docs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>json</category>
      <category>widget</category>
      <category>android</category>
      <category>design</category>
    </item>
  </channel>
</rss>
