<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Dev Penh]]></title><description><![CDATA[Dev Penh]]></description><link>https://devpenh.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 10:25:13 GMT</lastBuildDate><atom:link href="https://devpenh.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Draw an SVG icon in Flutter without a package: SVG path commands to CustomPainter]]></title><description><![CDATA[flutter_svg is the right answer when you need to show a lot of SVG files. But for a single icon you want to animate, recolour per path, clip with, or ship…]]></description><link>https://devpenh.hashnode.dev/draw-an-svg-icon-in-flutter-without-a-package-svg-path-commands-to-custompainter</link><guid isPermaLink="true">https://devpenh.hashnode.dev/draw-an-svg-icon-in-flutter-without-a-package-svg-path-commands-to-custompainter</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[SVG]]></category><dc:creator><![CDATA[Yorn Chanpenh]]></dc:creator><pubDate>Fri, 06 Nov 2026 06:37:41 GMT</pubDate><content:encoded><![CDATA[<p><code>flutter_svg</code> is the right answer when you need to show a lot of SVG files. But for a single icon you want to animate, recolour per path, clip with, or ship with zero dependencies, a plain <code>CustomPainter</code> is often nicer. The good news: SVG path data maps almost one to one onto Flutter's <code>Path</code> API.</p>
<h3>The command table</h3>
<table>
<thead>
<tr>
<th>SVG</th>
<th>Flutter <code>Path</code></th>
</tr>
</thead>
<tbody><tr>
<td><code>M x y</code></td>
<td><code>moveTo(x, y)</code></td>
</tr>
<tr>
<td><code>L x y</code></td>
<td><code>lineTo(x, y)</code></td>
</tr>
<tr>
<td><code>H x</code> / <code>V y</code></td>
<td><code>lineTo</code> with the other coordinate unchanged</td>
</tr>
<tr>
<td><code>C x1 y1 x2 y2 x y</code></td>
<td><code>cubicTo(x1, y1, x2, y2, x, y)</code></td>
</tr>
<tr>
<td><code>Q x1 y1 x y</code></td>
<td><code>quadraticBezierTo(x1, y1, x, y)</code></td>
</tr>
<tr>
<td><code>A rx ry rot large sweep x y</code></td>
<td><code>arcToPoint(Offset(x, y), radius: Radius.elliptical(rx, ry), rotation: rot, largeArc: large == 1, clockwise: sweep == 1)</code></td>
</tr>
<tr>
<td><code>Z</code></td>
<td><code>close()</code></td>
</tr>
</tbody></table>
<p>Lowercase (relative) commands have <code>relative…</code> versions (<code>relativeLineTo</code>, <code>relativeCubicTo</code>, <code>relativeArcToPoint</code> and so on), or you can add the current point yourself.</p>
<p>Two commands have no direct method:</p>
<ul>
<li><code>S</code> and <code>T</code> are "smooth" curves. Their first control point is the previous curve's last control point mirrored around the current point: <code>x1 = 2 * currentX - previousX2</code>, <code>y1 = 2 * currentY - previousY2</code>. If the previous command was not a curve of the same kind, the control point is the current point.</li>
<li><code>A</code> looks scary, but Flutter's <code>arcToPoint</code> takes exactly SVG's endpoint parameters. The only translation is the flags: <code>large-arc-flag</code> → <code>largeArc</code>, <code>sweep-flag</code> 1 → <code>clockwise: true</code>. <code>rotation</code> is in degrees, like SVG. A radius of 0 means a straight line in SVG, so draw a <code>lineTo</code> instead.</li>
</ul>
<h3>Scaling the viewBox</h3>
<p>SVG coordinates live in the <code>viewBox</code>. To fit them into whatever size the widget gets, scale uniformly and centre, which is what browsers do by default (<code>xMidYMid meet</code>):</p>
<pre><code class="language-dart">final scale = math.min(size.width / 48, size.height / 48);
canvas.translate((size.width - 48 * scale) / 2, (size.height - 48 * scale) / 2);
canvas.scale(scale);
</code></pre>
<p>If the viewBox does not start at <code>0 0</code>, add <code>canvas.translate(-minX, -minY)</code> after the scale.</p>
<h3>A full example</h3>
<p>This icon:</p>
<pre><code class="language-xml">&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none"&gt;
  &lt;rect x="4" y="4" width="40" height="40" rx="8" fill="#1E88E5"/&gt;
  &lt;path d="M14 25L21 32L35 17" stroke="white" stroke-width="4"
        stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;
</code></pre>
<p>becomes:</p>
<pre><code class="language-dart">import 'dart:math' as math;

import 'package:flutter/widgets.dart';

class CheckIconPainter extends CustomPainter {
  const CheckIconPainter();

  @override
  void paint(Canvas canvas, Size size) {
    final scale = math.min(size.width / 48, size.height / 48);
    canvas.translate((size.width - 48 * scale) / 2, (size.height - 48 * scale) / 2);
    canvas.scale(scale);

    // rect with rx="8": straight edges joined by arcs
    final path0 = Path()
      ..moveTo(12, 4)
      ..lineTo(36, 4)
      ..arcToPoint(const Offset(44, 12), radius: const Radius.circular(8))
      ..lineTo(44, 36)
      ..arcToPoint(const Offset(36, 44), radius: const Radius.circular(8))
      ..lineTo(12, 44)
      ..arcToPoint(const Offset(4, 36), radius: const Radius.circular(8))
      ..lineTo(4, 12)
      ..arcToPoint(const Offset(12, 4), radius: const Radius.circular(8))
      ..close();
    canvas.drawPath(path0, Paint()..color = const Color(0xFF1E88E5));

    final path1 = Path()
      ..moveTo(14, 25)
      ..lineTo(21, 32)
      ..lineTo(35, 17);
    canvas.drawPath(
      path1,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 4
        ..color = const Color(0xFFFFFFFF)
        ..strokeCap = StrokeCap.round
        ..strokeJoin = StrokeJoin.round,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) =&gt; false;
}
</code></pre>
<p>Use it with <code>CustomPaint(size: const Size(48, 48), painter: const CheckIconPainter())</code>.</p>
<h3>Gotchas worth knowing</h3>
<ul>
<li><strong>Default fill is black.</strong> An SVG shape with no <code>fill</code> attribute is filled black, and <code>stroke</code> defaults to none. <code>fill="none"</code> on the root is inherited by every child.</li>
<li><strong><code>&lt;line&gt;</code> is never filled</strong>, even if a fill is inherited. Only its stroke draws.</li>
<li><strong>SVG colours are alpha-last, Flutter's are alpha-first.</strong> <code>#1E88E580</code> in SVG is <code>Color(0x801E88E5)</code> in Flutter.</li>
<li><strong>Group opacity</strong> applies to the group as a whole in SVG. Multiplying it into each child's colour is close, but overlapping children inside a translucent group will look slightly different.</li>
<li><strong>Text, gradients and <code>&lt;use&gt;</code> need work first.</strong> Convert text to paths in your editor, and expand clones.</li>
</ul>
<h3>How I checked the mapping</h3>
<p>I wrote a small converter and tested it the boring way: twelve SVGs covering every path command, all four arc flag combinations, rotated and too-small arc radii, basic shapes, nested transforms and real Figma and Inkscape exports were drawn by the generated painters in <code>flutter test</code> (via <code>PictureRecorder</code> → <code>toImage</code>) and compared pixel by pixel with Chrome's rendering of the same files. Differences stayed under 0.6% of pixels, all on anti-aliased edges. Flipping a single arc flag on purpose produced 6.8% strongly different pixels, so the check does catch real mistakes.</p>
<p>The converter is free and runs in the browser (disclosure: I built it): <a href="https://devpenh.com/tools/svg-to-flutter/">SVG to Flutter CustomPainter</a>. It lists anything it could not convert instead of silently dropping it.</p>
<p><em>This post was written with AI assistance; the code was run and checked before publishing.</em></p>
]]></content:encoded></item><item><title><![CDATA[Why your Flutter JSON model crashes at runtime (and how to generate one that doesn't)]]></title><description><![CDATA[You write a model, the first API response parses fine, and a week later a crash report says: Nothing in your code changed. The data did. Here are the three…]]></description><link>https://devpenh.hashnode.dev/why-your-flutter-json-model-crashes-at-runtime-and-how-to-generate-one-that-doesn-t</link><guid isPermaLink="true">https://devpenh.hashnode.dev/why-your-flutter-json-model-crashes-at-runtime-and-how-to-generate-one-that-doesn-t</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[json]]></category><dc:creator><![CDATA[Yorn Chanpenh]]></dc:creator><pubDate>Fri, 06 Nov 2026 06:36:01 GMT</pubDate><content:encoded><![CDATA[<p>You write a model, the first API response parses fine, and a week later a crash report says:</p>
<pre><code class="language-plaintext">type 'int' is not a subtype of type 'double' in type cast
</code></pre>
<p>Nothing in your code changed. The data did. Here are the three type errors I see most often with <code>jsonDecode</code>, why they happen, and how to write (or generate) models that survive real responses.</p>
<h3>1. <code>int</code> vs <code>double</code></h3>
<p>Dart's <code>jsonDecode</code> reads the number type from the text. <code>1.0</code> is a <code>double</code>, <code>1</code> is an <code>int</code>:</p>
<pre><code class="language-dart">import 'dart:convert';

void main() {
  final a = jsonDecode('{"price": 1.0}') as Map&lt;String, dynamic&gt;;
  print(a['price'].runtimeType); // double

  final b = jsonDecode('{"price": 1}') as Map&lt;String, dynamic&gt;;
  final price = b['price'] as double;
  // type 'int' is not a subtype of type 'double' in type cast
}
</code></pre>
<p>Many backends drop the <code>.0</code> for whole numbers, so a price of <code>3.0</code> arrives as <code>3</code>. The safe cast for any field that is "a number with decimals" is:</p>
<pre><code class="language-dart">final price = (json['price'] as num).toDouble();
</code></pre>
<h3>2. <code>List&lt;dynamic&gt;</code> vs <code>List&lt;String&gt;</code></h3>
<p><code>jsonDecode</code> has no idea what your list should contain, so every JSON array becomes <code>List&lt;dynamic&gt;</code>:</p>
<pre><code class="language-dart">final json = jsonDecode('{"tags": ["a", "b"]}') as Map&lt;String, dynamic&gt;;
final tags = json['tags'] as List&lt;String&gt;;
// type 'List&lt;dynamic&gt;' is not a subtype of type 'List&lt;String&gt;' in type cast
</code></pre>
<p>Cast each item instead of the list:</p>
<pre><code class="language-dart">final tags = (json['tags'] as List&lt;dynamic&gt;).map((e) =&gt; e as String).toList();
</code></pre>
<p>Lists of objects work the same way, with <code>fromJson</code> on each item:</p>
<pre><code class="language-dart">final items = (json['items'] as List&lt;dynamic&gt;)
    .map((e) =&gt; Item.fromJson(e as Map&lt;String, dynamic&gt;))
    .toList();
</code></pre>
<h3>3. The key that is only sometimes there</h3>
<p>You build the model from one sample response. In that sample every item has a <code>note</code>. In production some items don't, and <code>json['note'] as String</code> throws because the value is <code>null</code>. Any key that can be missing or <code>null</code> needs a nullable type, <code>String?</code>, and a non-<code>required</code> constructor parameter.</p>
<p>The fix is to look at more than one sample. If a key is missing from any item, or <code>null</code> in any item, it's nullable.</p>
<h3>A model that handles all three</h3>
<p>Given this sample, where the second item has a <code>note</code> and the first doesn't:</p>
<pre><code class="language-json">{
  "id": 7,
  "price": 1.0,
  "tags": ["new"],
  "items": [
    { "sku": "A1", "qty": 2 },
    { "sku": "B2", "qty": 1, "note": "gift" }
  ]
}
</code></pre>
<p>a model that survives real responses looks like this:</p>
<pre><code class="language-dart">class Order {
  final int id;
  final double price;
  final List&lt;String&gt; tags;
  final List&lt;Item&gt; items;

  const Order({
    required this.id,
    required this.price,
    required this.tags,
    required this.items,
  });

  factory Order.fromJson(Map&lt;String, dynamic&gt; json) =&gt; Order(
        id: json['id'] as int,
        price: (json['price'] as num).toDouble(),
        tags: (json['tags'] as List&lt;dynamic&gt;).map((e) =&gt; e as String).toList(),
        items: (json['items'] as List&lt;dynamic&gt;).map((e) =&gt; Item.fromJson(e as Map&lt;String, dynamic&gt;)).toList(),
      );

  Map&lt;String, dynamic&gt; toJson() =&gt; {
        'id': id,
        'price': price,
        'tags': tags,
        'items': items.map((e) =&gt; e.toJson()).toList(),
      };
}

class Item {
  final String sku;
  final int qty;
  final String? note;

  const Item({
    required this.sku,
    required this.qty,
    this.note,
  });

  factory Item.fromJson(Map&lt;String, dynamic&gt; json) =&gt; Item(
        sku: json['sku'] as String,
        qty: json['qty'] as int,
        note: json['note'] as String?,
      );

  Map&lt;String, dynamic&gt; toJson() =&gt; {
        'sku': sku,
        'qty': qty,
        'note': note,
      };
}
</code></pre>
<p>Decoding <code>{"id": 8, "price": 3, "tags": [], "items": [{"sku": "C3", "qty": 1}]}</code> with it gives <code>price</code> = <code>3.0</code> and <code>note</code> = <code>null</code>, with no exceptions.</p>
<h3>Generating it</h3>
<p>I wrote that model by hand far too many times, so I built a free <a href="https://devpenh.com/tools/json-to-dart/">JSON to Dart generator</a> (disclosure: it's my project). The model above is its actual output for the sample. It:</p>
<ul>
<li>runs entirely in the browser, so API responses are never uploaded,</li>
<li>reads number types from the raw text, so <code>1.0</code> stays a <code>double</code> (JavaScript's <code>JSON.parse</code> would turn it into <code>1</code>),</li>
<li>merges the keys of every item in an array and makes keys that are missing or <code>null</code> in some items nullable,</li>
<li>outputs plain Dart (no packages), json_serializable or freezed,</li>
<li>can repair a map printed by <code>print()</code> / <code>debugPrint</code> (like <code>{id: 1, name: Ada}</code>) into JSON first.</li>
</ul>
<p>It does not guess <code>DateTime</code> or enums; dates stay <code>String</code>, so convert those by hand.</p>
<p>If you're debugging one of these errors right now, the guide <a href="https://devpenh.com/guides/flutter-json-type-errors/">Flutter JSON Errors: "is not a subtype" Explained</a> covers more of them, including <code>'_Map&lt;String, dynamic&gt;' is not a subtype of type ...</code>.</p>
<p><em>This post was written with AI assistance; the code was run and checked before publishing.</em></p>
]]></content:encoded></item><item><title><![CDATA[Your Dart model is right for one JSON response and wrong for the next]]></title><description><![CDATA[Generating a Dart model from one sample response is fast, and it is usually wrong in a few places. Here is a product item from an API: A model built from…]]></description><link>https://devpenh.hashnode.dev/your-dart-model-is-right-for-one-json-response-and-wrong-for-the-next</link><guid isPermaLink="true">https://devpenh.hashnode.dev/your-dart-model-is-right-for-one-json-response-and-wrong-for-the-next</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[json]]></category><dc:creator><![CDATA[Yorn Chanpenh]]></dc:creator><pubDate>Fri, 06 Nov 2026 06:34:23 GMT</pubDate><content:encoded><![CDATA[<p>Generating a Dart model from one sample response is fast, and it is usually wrong in a few places. Here is a product item from an API:</p>
<pre><code class="language-json">{
  "id": 102,
  "name": "USB-C cable",
  "price": 5,
  "discount": 0.15,
  "tags": [],
  "note": "Ships in 2 days"
}
</code></pre>
<p>A model built from this item alone guesses:</p>
<pre><code class="language-dart">price: json['price'] as int,                        // 5 looked like an int
discount: (json['discount'] as num).toDouble(),     // never null in this item
tags: json['tags'] as List&lt;dynamic&gt;,                // [] says nothing about the items
note: json['note'] as String,                       // missing from other items
</code></pre>
<p>The next item has <code>"price": 24.5</code> and <code>"discount": null</code>, and decoding it fails:</p>
<pre><code class="language-plaintext">type 'double' is not a subtype of type 'int' in type cast
</code></pre>
<p>Fix that and it fails on the next field:</p>
<pre><code class="language-dart">type 'Null' is not a subtype of type 'num' in type cast
</code></pre>
<p>The rule of thumb under sound null safety:</p>
<table>
<thead>
<tr>
<th>In your samples, the key is</th>
<th>Field type</th>
</tr>
</thead>
<tbody><tr>
<td>Present with a value in every item</td>
<td><code>required String name</code></td>
</tr>
<tr>
<td>Missing from some items</td>
<td><code>String? note</code></td>
</tr>
<tr>
<td><code>null</code> in some items</td>
<td><code>double? discount</code></td>
</tr>
<tr>
<td>A whole number in one item, fractional in another</td>
<td><code>double</code>, read with <code>(json['price'] as num).toDouble()</code></td>
</tr>
</tbody></table>
<p>So build models from several real responses, not one: the empty list, the item without a discount, the one with a note. The full guide covers <code>late</code> (and why it does not help here), nested lists, dates and json_serializable vs freezed: <a href="https://devpenh.com/guides/json-to-dart-null-safety/">https://devpenh.com/guides/json-to-dart-null-safety/</a></p>
<p>Disclosure: I built the free JSON to Dart generator on that site (<a href="https://devpenh.com/tools/json-to-dart/">https://devpenh.com/tools/json-to-dart/</a>). Paste an array of items and it merges them, so keys missing from some items come out nullable. It runs in the browser; nothing is uploaded.</p>
<p><em>This post was written with AI and published at the author's request. Every code example and error message was run on Dart 3.13.7.</em></p>
]]></content:encoded></item><item><title><![CDATA[401 Unauthorized with a valid JWT? Five causes I check first]]></title><description><![CDATA["The token is fine, it works in Postman, but the API returns 401." A 401 with a JWT is one of two problems: the token never reached the verifier in the…]]></description><link>https://devpenh.hashnode.dev/401-unauthorized-with-a-valid-jwt-five-causes-i-check-first</link><guid isPermaLink="true">https://devpenh.hashnode.dev/401-unauthorized-with-a-valid-jwt-five-causes-i-check-first</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Yorn Chanpenh]]></dc:creator><pubDate>Fri, 06 Nov 2026 06:32:11 GMT</pubDate><content:encoded><![CDATA[<p>"The token is fine, it works in Postman, but the API returns 401." A 401 with a JWT is one of two problems: the token never reached the verifier in the right shape, or it arrived and failed a check. These are the causes I check first. Every error message below was run on Node.js 24 with <code>jsonwebtoken</code> 9.0.2.</p>
<p><strong>1. Read the response header first</strong></p>
<p>A good API says why it rejected you:</p>
<pre><code class="language-http">HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="token expired"
</code></pre>
<p>No <code>error</code> attribute usually means the server found no token at all. <code>error="invalid_token"</code> means it found one and verification failed.</p>
<p><strong>2. The header is not exactly <code>Bearer &lt;token&gt;</code></strong></p>
<p>All of these fail inside the library with the same unhelpful message:</p>
<pre><code class="language-js">jwt.verify("Bearer " + token, secret);     // JsonWebTokenError: invalid token
jwt.verify(JSON.stringify(token), secret); // JsonWebTokenError: invalid token  (quotes from localStorage)
jwt.verify(token + "\n", secret);          // JsonWebTokenError: invalid token  (copied from a terminal)
jwt.verify("", secret);                    // JsonWebTokenError: jwt must be provided
</code></pre>
<p><code>Bearer undefined</code> on the client ends up as the last one.</p>
<p><strong>3. The verifier and the token disagree</strong></p>
<pre><code class="language-plaintext">invalid signature                                    -&gt; different secret/key, often another environment
jwt expired                                          -&gt; exp is in the past
jwt audience invalid. expected: api://orders         -&gt; token issued for another API
jwt issuer invalid. expected: https://auth.example.com -&gt; staging token sent to production
invalid algorithm                                    -&gt; RS256 token, HS256 verifier
</code></pre>
<p>Decode the token (no secret needed) and compare <code>alg</code>, <code>aud</code>, <code>iss</code> and <code>exp</code> with your verify options.</p>
<p><strong>4. It only fails in the browser</strong></p>
<p>If curl works and the browser shows a CORS error, look for the <code>OPTIONS</code> preflight. Browsers never attach the token to it, so an auth middleware that runs first answers 401 and the browser reports a CORS failure. Put CORS before auth:</p>
<pre><code class="language-js">app.use(cors({ origin: "http://localhost:5173" }));
app.use(requireAuth);
</code></pre>
<p><strong>5. A redirect dropped the header</strong></p>
<p><code>fetch</code> (browsers and Node) removes <code>Authorization</code> when a redirect goes to another origin, so an <code>http</code> to <code>https</code> or a host redirect can silently strip it.</p>
<p>One trap that does <em>not</em> give a 401: an <code>exp</code> set with <code>Date.now()</code> (milliseconds) passes verification, because the token now expires tens of thousands of years from now.</p>
<p>The full checklist, with a tested Express middleware that returns a useful <code>WWW-Authenticate</code> header, is here: <a href="https://devpenh.com/guides/debug-401-unauthorized-jwt/">https://devpenh.com/guides/debug-401-unauthorized-jwt/</a></p>
<p>Disclosure: I run that site. Its JWT decoder (<a href="https://devpenh.com/tools/jwt-decoder/">https://devpenh.com/tools/jwt-decoder/</a>) decodes tokens in the browser, so you can inspect a real token from a failing request without sending it anywhere.</p>
<p><em>This post was written with AI assistance; the code and error messages were run and checked before publishing.</em></p>
]]></content:encoded></item></channel></rss>