<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Rody Davis</title><description>Rody Davis&apos;s personal website and portfolio.</description><link>https://rodydavis.com/</link><language>en-us</language><item><title>About Me</title><link>https://rodydavis.com/about/</link><guid isPermaLink="true">https://rodydavis.com/about/</guid><description>Rody Davis is a Senior Developer Relations Engineer at Google, passionate about VR/AR, Web Components, and Dart/Flutter packages, and a musician and podcaster based in San Francisco.</description><pubDate>Sat, 18 Jan 2025 19:18:39 GMT</pubDate><content:encoded>&lt;h1&gt;About Me&lt;/h1&gt;
&lt;h2&gt;Discover&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;🕹 Fascinated by &lt;a href=&quot;https://aframe.io/&quot;&gt;VR/AR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🤓 Working on &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Web_Components&quot;&gt;Web Componenets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🥳 Senior Developer Advocate for &lt;a href=&quot;https://antigravity.google&quot;&gt;Antigravity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🎹 Musician &lt;a href=&quot;https://soundcloud.com/theonlysounddr&quot;&gt;SoundCloud&lt;/a&gt;, &lt;a href=&quot;https://open.spotify.com/artist/5HBkYdhRZn1aOq40T2A7Eg&quot;&gt;Spotify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🎧 Podcast &amp;quot;Creative Engineering&amp;quot;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://podcasts.apple.com/us/podcast/creative-engineering/id1507852833&quot;&gt;Apple Podcasts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://open.spotify.com/show/3UTiK34aDOOSHFpGQ0RglN&quot;&gt;Spotify Podcasts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://music.amazon.com/podcasts/8884a5cb-a92a-4ba5-a3ef-906ac334386d/Creative-Engineering?ref=dm_wcp_pp_link_pr_s&quot;&gt;Amazon Music&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;⚒️ Creator of &lt;a href=&quot;https://widget.studio/&quot;&gt;Widget Studio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📦 Dart/Flutter &lt;a href=&quot;https://pub.dev/publishers/rodydavis.com/packages&quot;&gt;Packages&lt;/a&gt; on &lt;a href=&quot;https://pub.dev/&quot;&gt;pub.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;😎 Find me on &lt;a href=&quot;https://glitch.com/@rodydavis&quot;&gt;Glitch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🗣 Follow on &lt;a href=&quot;https://twitter.com/rodydavis&quot;&gt;Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📸 Follow me on &lt;a href=&quot;https://instagram.com/rodydavisjr?r=nametag&quot;&gt;Instagram&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📹 Subscribe on &lt;a href=&quot;https://www.youtube.com/rodydavis&quot;&gt;YouTube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📖 Read on &lt;a href=&quot;https://medium.com/@rody.davis.jr&quot;&gt;Medium&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/profile_8v0z7aduoc.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;My name is Rody Davis and I grew up in Alabama and currently work at Google on the &lt;a href=&quot;https://antigravity.google&quot;&gt;Antigravity&lt;/a&gt; team as a Senior Developer Relations Engineer and live in San Francisco.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/personal_golden_gate_9y3fwiwicv.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Growing up, I was always taking apart every device I had and recreating it to figure it out and how it worked then sometimes making something new. I always wanted to be an inventor growing up, and I would sketch out designs and build prototypes and stuff them in a box. In high school I was in marching band on the drumline and worked many jobs.&lt;/p&gt;
&lt;p&gt;I went to Florida College with the intention of being an Audio Visual Technician. After working that job for almost five years, I joined a small startup where I managed the video department and later became the System Administrator.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/personal_grand_canyon_5wbn12eol9.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Eventually I realized that this was not the job I wanted anymore, so I slowly but steadily taught my self mobile development. I created my first app and released it to the AppStore- “The Pitch Pipe”- and it quickly gained traction as it fulfilled a need in the market being overlooked. I was then able to get a job as a professional mobile developer and now I am in charge of the mobile department creating both Android and iOS apps.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/personal_shuttle_o0dk6jemz6.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Something that dawned on me recently is that I am doing my dream job. I always wanted to be an inventor and now I am. It may not involve physical products but the rules still apply. I get to create products from pure imagination and bring them to market.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/personal_family_qxtsx99czk.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;I am also engaged to the love of my life, Molly ❤️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/riyakataria_rody_molly_031823_20_1_r5q7mluc1z.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;I am looking forward to what the future will bring and I’m excited about all the new technologies coming up. All the apps I create reflect what I am learning and trying to perfect with the different markets and technologies for mobile. We spend a majority of our lives on our smart phones, so there is a need now more than ever for high quality and functioning apps.&lt;/p&gt;
&lt;p&gt;Never stop learning and trying to be better.&lt;/p&gt;
&lt;h2&gt;Social&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://glitch.com/@rodydavis&quot;&gt;Glitch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rodydavis&quot;&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/users/7303311/rody-davis&quot;&gt;StackOverflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://twitter.com/rodydavis&quot;&gt;Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtube.com/rodydavis&quot;&gt;YouTube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://instagram.com/rodydavisjr&quot;&gt;Instagram&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://facebook.com/rodydavisjr&quot;&gt;Facebook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.linkedin.com/in/rodydavis&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tiktok.com/@rodydavisjr&quot;&gt;TikTok&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/rodydavis&quot;&gt;Dev.to&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rodydavis.medium.com/&quot;&gt;Medium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;mailto:rody.davis.jr@gmail.com&quot;&gt;Email&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Support&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.buymeacoffee.com/rodydavis&quot;&gt;Buy Me A Coffee&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;amp;hosted_button_id=WSH3GVC49GNNJ&quot;&gt;PayPal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Home</title><link>https://rodydavis.com/welcome/</link><guid isPermaLink="true">https://rodydavis.com/welcome/</guid><description>Rody Davis is a Senior Developer Advocate for AntiGravity, building Web Components and Dart/Flutter packages, and is also a musician and podcast host.</description><pubDate>Wed, 20 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Rody Davis&lt;/h1&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;🕹 Fascinated by &lt;a href=&quot;https://aframe.io/&quot;&gt;VR/AR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🤓 Building &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Web_Components&quot;&gt;Web Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🥳 Senior Developer Advocate for &lt;a href=&quot;https://antigravity.google&quot;&gt;Antigravity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🎹 Musician &lt;a href=&quot;https://soundcloud.com/theonlysounddr&quot;&gt;SoundCloud&lt;/a&gt;, &lt;a href=&quot;https://open.spotify.com/artist/5HBkYdhRZn1aOq40T2A7Eg&quot;&gt;Spotify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🎧 Podcast &amp;quot;Creative Engineering&amp;quot; &lt;a href=&quot;https://podcasts.apple.com/us/podcast/creative-engineering/id1507852833&quot;&gt;Apple Podcasts&lt;/a&gt;, &lt;a href=&quot;https://open.spotify.com/show/3UTiK34aDOOSHFpGQ0RglN&quot;&gt;Spotify Podcasts&lt;/a&gt;, &lt;a href=&quot;https://music.amazon.com/podcasts/8884a5cb-a92a-4ba5-a3ef-906ac334386d/Creative-Engineering?ref=dm_wcp_pp_link_pr_s&quot;&gt;Amazon Music&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;⚒️ Creator of &lt;a href=&quot;https://widget.studio/&quot;&gt;Widget Studio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📦 Dart/Flutter &lt;a href=&quot;https://pub.dev/publishers/rodydavis.com/packages&quot;&gt;Packages&lt;/a&gt; on &lt;a href=&quot;https://pub.dev&quot;&gt;pub.dev&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Social&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://glitch.com/@rodydavis&quot;&gt;Glitch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rodydavis&quot;&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/users/7303311/rody-davis&quot;&gt;StackOverflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://twitter.com/rodydavis&quot;&gt;Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtube.com/rodydavis&quot;&gt;YouTube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://instagram.com/rodydavisjr&quot;&gt;Instagram&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://facebook.com/rodydavisjr&quot;&gt;Facebook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.linkedin.com/in/rodydavis&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tiktok.com/@rodydavisjr&quot;&gt;TikTok&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/rodydavis&quot;&gt;Dev.to&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rodydavis.medium.com/&quot;&gt;Medium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;mailto:rody.davis.jr@gmail.com&quot;&gt;Email&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Recently Played&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://spotify-recently-played-readme.vercel.app/api?user=1240938184&quot; alt=&quot;Alt text&quot;&gt;&lt;/p&gt;
</content:encoded></item><item><title>How to Deploy PocketBase to Cloud Run</title><link>https://rodydavis.com/backend/pocketbase-cloudrun/</link><guid isPermaLink="true">https://rodydavis.com/backend/pocketbase-cloudrun/</guid><description>Deploy PocketBase to Google Cloud Run by leveraging volume mounts for persistent storage and scaling, enabling features like zero scaling and infinite storage.</description><pubDate>Sat, 18 Jan 2025 22:12:22 GMT</pubDate><content:encoded>&lt;h1&gt;How to Deploy PocketBase to Cloud Run&lt;/h1&gt;
&lt;p&gt;It is now possible to run &lt;a href=&quot;https://pocketbase.io/&quot;&gt;PocketBase&lt;/a&gt; on Google &lt;a href=&quot;https://cloud.google.com/run?hl=en&quot;&gt;CloudRun&lt;/a&gt; because of the recent support for &lt;a href=&quot;https://cloud.google.com/run/docs/configuring/services/cloud-storage-volume-mounts&quot;&gt;mounting volumes&lt;/a&gt;. This is a guide on how to deploy PocketBase on Google Cloud Run.&lt;/p&gt;
&lt;h2&gt;Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Scale to zero&lt;/li&gt;
&lt;li&gt;Infinite storage (and file deletion protection, file versions, and multi region)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pb_data&lt;/code&gt;/&lt;code&gt;pb_public&lt;/code&gt;/&lt;code&gt;pb_hooks&lt;/code&gt; all in the same file system&lt;/li&gt;
&lt;li&gt;Backups can be done either by PocketBase or by protecting the bucket&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Google Cloud project&lt;/li&gt;
&lt;li&gt;Google Cloud Storage bucket&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;Fork &lt;a href=&quot;https://github.com/rodydavis/pocketbase-cloudrun/tree/main&quot;&gt;this repository&lt;/a&gt; or click &amp;quot;Use this template&amp;quot; to create your own repository.&lt;/p&gt;
&lt;h2&gt;Steps&lt;/h2&gt;
&lt;h3&gt;Create a new service&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/pb_cloud_run_1_3qclbfac0c.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Google Cloud Build&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Setup with Cloud Build
&lt;ul&gt;
&lt;li&gt;Repository Provider: &lt;code&gt;GitHub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Select Repository: &lt;code&gt;THIS_REPOSITORY_FORK&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Branch: &lt;code&gt;main&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Build Configuration: &lt;code&gt;Dockerfile&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;General Settings&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Allow unauthenticated invocations&lt;/li&gt;
&lt;li&gt;CPU is only allocated when the service is handling requests&lt;/li&gt;
&lt;li&gt;Maximum number of requests per container is set to &lt;code&gt;1000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Maximum number of containers is set to &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Timeout is set to &lt;code&gt;3600&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Ingress is set to internal and &lt;code&gt;all&lt;/code&gt; traffic&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Container(s), Volumes, Networking, Security&lt;/h4&gt;
&lt;h5&gt;Volumes&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Add volume
&lt;ul&gt;
&lt;li&gt;Volume type: &lt;code&gt;Google Storage bucket&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Volume name: &lt;code&gt;remote-storage (or any name you want)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Bucket: &lt;code&gt;YOUR_BUCKET_NAME&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Read-only: &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Container(s)&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Startup CPU boost is &lt;code&gt;enabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Volume mount (s)
&lt;ul&gt;
&lt;li&gt;Volume name: &lt;code&gt;remote-storage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Mount path: &lt;code&gt;/cloud/storage&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Add Health Checks&lt;/h4&gt;
&lt;p&gt;You can add a health check to your service that uses Pocketbase&apos;s health check endpoint &lt;code&gt;/api/health&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/pb_cloud_run_2_7911u5glhr.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Deploy and Wait&lt;/h3&gt;
&lt;p&gt;Now create the service and wait for the cloud build to finish.&lt;/p&gt;
&lt;p&gt;If everything goes well, you should see the service deployed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/pb_cloud_run_3_y6n1ukkd8u.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;What if I have local files that I want to use?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pb_data&lt;/code&gt;, &lt;code&gt;pb_public&lt;/code&gt;, and &lt;code&gt;pb_hooks&lt;/code&gt; are all directories you might use during development.&lt;/p&gt;
&lt;p&gt;You can upload these directories to your Google Cloud Storage bucket you created earlier to the root directory.&lt;/p&gt;
&lt;h3&gt;Can I use a custom domain?&lt;/h3&gt;
&lt;p&gt;Yes, you can use a custom domain. You can follow the guide on the &lt;a href=&quot;https://cloud.google.com/run/docs/mapping-custom-domains&quot;&gt;official documentation&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Migrating my Blog to PocketBase</title><link>https://rodydavis.com/blog/migrating-to-pocketbase/</link><guid isPermaLink="true">https://rodydavis.com/blog/migrating-to-pocketbase/</guid><description>Migrating a blog to PocketBase, detailing the journey through various platforms like Wix, Github Pages, 11ty, Lit, and Obsidian, highlighting the desire for customization and simplification.</description><pubDate>Mon, 20 Jan 2025 20:08:23 GMT</pubDate><content:encoded>&lt;h1&gt;Migrating my Blog to PocketBase&lt;/h1&gt;
&lt;p&gt;As the plight of every developer, we must constantly improve our personal website and blog to reflect our latest passion and exploration.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/giphy_fa0f9ejfy5.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;...of course not all developers need to do this but it is a fun exercise none the less.&lt;/p&gt;
&lt;p&gt;I really enjoy being able to look at new tech stacks and find ways to simplify my setup or make it more fun to use.&lt;/p&gt;
&lt;h2&gt;Context&lt;/h2&gt;
&lt;p&gt;For those that have not been following my migrations, I have been an early adopter on various tech stacks and especially for my blog.&lt;/p&gt;
&lt;p&gt;My very first website for &lt;a href=&quot;https://%20rodydavis.com&quot;&gt;rodydavis.com&lt;/a&gt; was made with &lt;a href=&quot;https://wix.com/&quot;&gt;Wix&lt;/a&gt; which was great to get started but it was before I became a developer.&lt;/p&gt;
&lt;p&gt;I later migrated my blog to &lt;a href=&quot;https://pages.github.com/&quot;&gt;Github Pages&lt;/a&gt; with &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt; and was very happy with it and introduced me to &lt;a href=&quot;https://dev.to/dailydevtips1/what-exactly-is-frontmatter-123g&quot;&gt;Frontmatter&lt;/a&gt; and &lt;a href=&quot;https://google.github.io/styleguide/docguide/style.html&quot;&gt;Markdown&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This worked for awhile, but then I wanted to make it a lot more customized which is when I migrated to &lt;a href=&quot;https://www.11ty.dev/&quot;&gt;11ty&lt;/a&gt;. This was a dead simple migration as I just needed to move my blog posts and add some config for my website metadata.&lt;/p&gt;
&lt;p&gt;As much as I loved the customization, it became hard to make my blog depend on &lt;a href=&quot;https://nodejs.org/en&quot;&gt;Node&lt;/a&gt; and &lt;a href=&quot;https://www.npmjs.com/&quot;&gt;npm&lt;/a&gt; but I still adopted it and setup some &lt;a href=&quot;https://github.com/features/actions&quot;&gt;Github Actions&lt;/a&gt; to build my website when I push commits.&lt;/p&gt;
&lt;p&gt;I then migrated my blog to a custom solution based on &lt;a href=&quot;https://lit.dev/&quot;&gt;lit&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_components&quot;&gt;web components&lt;/a&gt; but did not last long as it was mostly just for exploration. In the past I even had a version of my website built with &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt; too.&lt;/p&gt;
&lt;p&gt;The next big migration was to &lt;a href=&quot;https://obsidian.md/&quot;&gt;Obsidian&lt;/a&gt; which allowed me to publish my website with &lt;a href=&quot;https://obsidian.md/publish&quot;&gt;Obsidian Publish&lt;/a&gt; very easily from my phone or laptop.&lt;/p&gt;
&lt;p&gt;This finally broke me free of needing to bundle my website and was very happy with this setup, which leads me to today...&lt;/p&gt;
&lt;h2&gt;PocketBase&lt;/h2&gt;
&lt;p&gt;For those that are not familiar, &lt;a href=&quot;https://pocketbase.io/&quot;&gt;PocketBase&lt;/a&gt; is a &lt;a href=&quot;https://go.dev/&quot;&gt;Go&lt;/a&gt; library that compiles to a single binary, backed by SQLite as an open source CMS.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/screenshot_2025_01_20_at_11_53_in1ca1lmdg.29AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;This makes it dead simple to deploy to a small VPS and and just point your custom domain to it with free SSL cert generation using &lt;a href=&quot;https://letsencrypt.org/&quot;&gt;Let&apos;s Encrypt&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Coolify&lt;/h2&gt;
&lt;p&gt;The other thing I am now doing is hosting my website on &lt;a href=&quot;https://www.coolify.io/&quot;&gt;Coolify&lt;/a&gt; via a &lt;a href=&quot;https://www.hetzner.com/&quot;&gt;Hetzner&lt;/a&gt; server.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/screenshot_2025_01_20_at_12_01_u1dsid5hmx.06PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;This has been a joy to use and makes it easy to deploy to VPS you own in &lt;a href=&quot;https://cloud.google.com/&quot;&gt;GCP&lt;/a&gt;, &lt;a href=&quot;https://www.digitalocean.com/&quot;&gt;Digitial Ocean&lt;/a&gt;, &lt;a href=&quot;https://www.hetzner.com/&quot;&gt;Hetzner&lt;/a&gt; or even a &lt;a href=&quot;https://www.raspberrypi.com/&quot;&gt;Rasberry PI&lt;/a&gt; 👀.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Now I can deploy my website with no build step (for the frontend) and have dynamic features like the &lt;a href=&quot;https://openheart.fyi/&quot;&gt;Open Heart Protocol&lt;/a&gt;, live view counts, dynamic tag pages, live editor on the web and generated RSS feed.&lt;/p&gt;
&lt;p&gt;If you want to see the &lt;strong&gt;source code&lt;/strong&gt; you can find it &lt;a href=&quot;https://github.com/rodydavis/rodydavis&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Privacy Policy</title><link>https://rodydavis.com/privacy/</link><guid isPermaLink="true">https://rodydavis.com/privacy/</guid><description>Rody Davis Productions&apos; privacy policy outlines how the website collects, uses, and protects user information, including non-personal data like browser type and potentially personally identifiable information submitted through the site.</description><pubDate>Sat, 18 Jan 2025 19:15:26 GMT</pubDate><content:encoded>&lt;h1&gt;Privacy Policy&lt;/h1&gt;
&lt;p&gt;Your privacy is critically important to us.&lt;/p&gt;
&lt;p&gt;Rody Davis Productions is located at:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;Rody Davis Productions
6142 Pinewood Rd
Oakland CA, United States
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is Rody Davis Productions&apos;s policy to respect your privacy regarding any information we may collect while operating our website. This Privacy Policy applies to &lt;a href=&quot;https://rodydavis.com/&quot;&gt;https://rodydavis.com&lt;/a&gt; (hereinafter, &amp;quot;us&amp;quot;, &amp;quot;we&amp;quot;, or &amp;quot;&lt;a href=&quot;https://rodydavis.com%22/&quot;&gt;https://rodydavis.com&amp;quot;&lt;/a&gt;). We respect your privacy and are committed to protecting personally identifiable information you may provide us through the Website. We have adopted this privacy policy (&amp;quot;Privacy Policy&amp;quot;) to explain what information may be collected on our Website, how we use this information, and under what circumstances we may disclose the information to third parties. This Privacy Policy applies only to information we collect through the Website and does not apply to our collection of information from other sources.&lt;br&gt;
This Privacy Policy, together with the Terms and conditions posted on our Website, set forth the general rules and policies governing your use of our Website. Depending on your activities when visiting our Website, you may be required to agree to additional terms and conditions.&lt;/p&gt;
&lt;h2&gt;Website Visitors&lt;/h2&gt;
&lt;p&gt;Like most website operators, Rody Davis Productions collects non-personally-identifying information of the sort that web browsers and servers typically make available, such as the browser type, language preference, referring site, and the date and time of each visitor request. Rody Davis Productions&apos;s purpose in collecting non-personally identifying information is to better understand how Rody Davis Productions&apos;s visitors use its website. From time to time, Rody Davis Productions may release non-personally-identifying information in the aggregate, e.g., by publishing a report on trends in the usage of its website.&lt;br&gt;
Rody Davis Productions also collects potentially personally-identifying information like Internet Protocol (IP) addresses for logged in users and for users leaving comments on &lt;a href=&quot;https://rodydavis.com/&quot;&gt;https://rodydavis.com&lt;/a&gt; blog posts. Rody Davis Productions only discloses logged in user and commenter IP addresses under the same circumstances that it uses and discloses personally-identifying information as described below.&lt;/p&gt;
&lt;h2&gt;Gathering of Personally-Identifying Information&lt;/h2&gt;
&lt;p&gt;Certain visitors to Rody Davis Productions&apos;s websites choose to interact with Rody Davis Productions in ways that require Rody Davis Productions to gather personally-identifying information. The amount and type of information that Rody Davis Productions gathers depends on the nature of the interaction. For example, we ask visitors who sign up for a blog at &lt;a href=&quot;https://rodydavis.com/&quot;&gt;https://rodydavis.com&lt;/a&gt; to provide a username and email address.&lt;/p&gt;
&lt;h2&gt;Security&lt;/h2&gt;
&lt;p&gt;The security of your Personal Information is important to us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While we strive to use commercially acceptable means to protect your Personal Information, we cannot guarantee its absolute security.&lt;/p&gt;
&lt;h2&gt;Advertisements&lt;/h2&gt;
&lt;p&gt;Ads appearing on our website may be delivered to users by advertising partners, who may set cookies. These cookies allow the ad server to recognize your computer each time they send you an online advertisement to compile information about you or others who use your computer. This information allows ad networks to, among other things, deliver targeted advertisements that they believe will be of most interest to you. This Privacy Policy covers the use of cookies by Rody Davis Productions and does not cover the use of cookies by any advertisers.&lt;/p&gt;
&lt;h2&gt;Links To External Sites&lt;/h2&gt;
&lt;p&gt;Our Service may contain links to external sites that are not operated by us. If you click on a third party link, you will be directed to that third party&apos;s site. We strongly advise you to review the Privacy Policy and terms and conditions of every site you visit.&lt;br&gt;
We have no control over, and assume no responsibility for the content, privacy policies or practices of any third party sites, products or services.&lt;/p&gt;
&lt;h2&gt;Protection of Certain Personally-Identifying Information&lt;/h2&gt;
&lt;p&gt;Rody Davis Productions discloses potentially personally-identifying and personally-identifying information only to those of its employees, contractors and affiliated organizations that (i) need to know that information in order to process it on Rody Davis Productions&apos;s behalf or to provide services available at Rody Davis Productions&apos;s website, and (ii) that have agreed not to disclose it to others. Some of those employees, contractors and affiliated organizations may be located outside of your home country; by using Rody Davis Productions&apos;s website, you consent to the transfer of such information to them. Rody Davis Productions will not rent or sell potentially personally-identifying and personally-identifying information to anyone. Other than to its employees, contractors and affiliated organizations, as described above, Rody Davis Productions discloses potentially personally-identifying and personally-identifying information only in response to a subpoena, court order or other governmental request, or when Rody Davis Productions believes in good faith that disclosure is reasonably necessary to protect the property or rights of Rody Davis Productions, third parties or the public at large.&lt;br&gt;
If you are a registered user of &lt;a href=&quot;https://rodydavis.com/&quot;&gt;https://rodydavis.com&lt;/a&gt; and have supplied your email address, Rody Davis Productions may occasionally send you an email to tell you about new features, solicit your feedback, or just keep you up to date with what&apos;s going on with Rody Davis Productions and our products. We primarily use our blog to communicate this type of information, so we expect to keep this type of email to a minimum. If you send us a request (for example via a support email or via one of our feedback mechanisms), we reserve the right to publish it in order to help us clarify or respond to your request or to help us support other users. Rody Davis Productions takes all measures reasonably necessary to protect against the unauthorized access, use, alteration or destruction of potentially personally-identifying and personally-identifying information.&lt;/p&gt;
&lt;h2&gt;Aggregated Statistics&lt;/h2&gt;
&lt;p&gt;Rody Davis Productions may collect statistics about the behavior of visitors to its website. Rody Davis Productions may display this information publicly or provide it to others. However, Rody Davis Productions does not disclose your personally-identifying information.&lt;/p&gt;
&lt;h2&gt;Cookies&lt;/h2&gt;
&lt;p&gt;To enrich and perfect your online experience, Rody Davis Productions uses &amp;quot;Cookies&amp;quot;, similar technologies and services provided by others to display personalized content, appropriate advertising and store your preferences on your computer.&lt;br&gt;
A cookie is a string of information that a website stores on a visitor&apos;s computer, and that the visitor&apos;s browser provides to the website each time the visitor returns. Rody Davis Productions uses cookies to help Rody Davis Productions identify and track visitors, their usage of &lt;a href=&quot;https://rodydavis.com/&quot;&gt;https://rodydavis.com&lt;/a&gt;, and their website access preferences. Rody Davis Productions visitors who do not wish to have cookies placed on their computers should set their browsers to refuse cookies before using Rody Davis Productions&apos;s websites, with the drawback that certain features of Rody Davis Productions&apos;s websites may not function properly without the aid of cookies.&lt;br&gt;
By continuing to navigate our website without changing your cookie settings, you hereby acknowledge and agree to Rody Davis Productions&apos;s use of cookies.&lt;/p&gt;
&lt;p&gt;Privacy Policy Changes&lt;br&gt;
Although most changes are likely to be minor, Rody Davis Productions may change its Privacy Policy from time to time, and in Rody Davis Productions&apos;s sole discretion. Rody Davis Productions encourages visitors to frequently check this page for any changes to its Privacy Policy. Your continued use of this site after any change in this Privacy Policy will constitute your acceptance of such change.&lt;/p&gt;
&lt;h2&gt;Credit &amp;amp; Contact Information&lt;/h2&gt;
&lt;p&gt;This privacy policy was created at &lt;a href=&quot;https://termsandconditionstemplate.com/privacy-policy-generator/&quot;&gt;https://termsandconditionstemplate.com/privacy-policy-generator/&lt;/a&gt;. If you have any questions about this Privacy Policy, please contact us via or phone.&lt;/p&gt;
</content:encoded></item><item><title>How to do Offline Recommendations with SQLite and Gemini</title><link>https://rodydavis.com/ai/vector-recommendations/</link><guid isPermaLink="true">https://rodydavis.com/ai/vector-recommendations/</guid><description>Learn how to implement offline content recommendations using Gemini for text embeddings and SQLite for vector storage, enabling efficient similarity searches for related content in applications like CMS platforms.</description><pubDate>Mon, 10 Feb 2025 12:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to do Offline Recommendations with SQLite and Gemini&lt;/h1&gt;
&lt;p&gt;When working with a CMS (like &lt;a href=&quot;https://pocketbase.io&quot;&gt;PocketBase&lt;/a&gt;) it is common to add some sort of recommendatios for related content. For example you can have a list of blog posts, and show related posts either by random selection or recently viewed.&lt;/p&gt;
&lt;p&gt;I first learned about this technique from &lt;a href=&quot;https://aaronfrancis.com&quot;&gt;Aaron Francis&lt;/a&gt; on his YouTube channel:&lt;/p&gt;
&lt;h2&gt;Text Embeddings&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/embeddings&quot;&gt;Text embeddings&lt;/a&gt; are a way to convert a chunk of text into a an array of numbers. Having a mathematical representation means we can easily store them in a database and run common functions to calculate the distances between vectors that we have stored.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You will need an &lt;a href=&quot;https://aistudio.google.com/apikey&quot;&gt;API Key from AI Studio&lt;/a&gt; to generate the descriptions and embeddings.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In order to create the embedding we need to first generate chunk small enough to fit in the embedding window size. For example we can use an LLM like &lt;a href=&quot;https://ai.google.dev/gemini-api/docs/text-generation?lang=go&quot;&gt;Gemini to generate a description&lt;/a&gt; for a blog post and then vectorize the description which we can store in the database.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We only need to generate a new embedding and description when the content changes which limits the billing costs to the frequency of the content changes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Storing the Vectors&lt;/h2&gt;
&lt;p&gt;To store the text embeddings as vectors we can save them in a &lt;a href=&quot;https://www.sqlite.org&quot;&gt;SQLite&lt;/a&gt; database using a &lt;a href=&quot;https://www.sqlite.org/loadext.html&quot;&gt;runtime loadable extension&lt;/a&gt; called &lt;a href=&quot;https://github.com/asg017/sqlite-vec&quot;&gt;sqlite-vec&lt;/a&gt;. Here is an example from the readme on how to query the vectors directly in SQLite:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;.load ./vec0

create virtual table vec_examples using vec0(
  sample_embedding float[8]
);

-- vectors can be provided as JSON or in a compact binary format
insert into vec_examples(rowid, sample_embedding)
  values
    (1, &apos;[-0.200, 0.250, 0.341, -0.211, 0.645, 0.935, -0.316, -0.924]&apos;),
    (2, &apos;[0.443, -0.501, 0.355, -0.771, 0.707, -0.708, -0.185, 0.362]&apos;),
    (3, &apos;[0.716, -0.927, 0.134, 0.052, -0.669, 0.793, -0.634, -0.162]&apos;),
    (4, &apos;[-0.710, 0.330, 0.656, 0.041, -0.990, 0.726, 0.385, -0.958]&apos;);


-- KNN style query
select
  rowid,
  distance
from vec_examples
where sample_embedding match &apos;[0.890, 0.544, 0.825, 0.961, 0.358, 0.0196, 0.521, 0.175]&apos;
order by distance
limit 2;
/*
┌───────┬──────────────────┐
│ rowid │     distance     │
├───────┼──────────────────┤
│ 2     │ 2.38687372207642 │
│ 1     │ 2.38978505134583 │
└───────┴──────────────────┘
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can just take the vectors we created earlier and store them in a table which can update as content changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE posts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  description TEXT.
  embeddings TEXT
);

CREATE VIRTUAL TABLE vec_posts USING vec0(
  id INTEGER PRIMARY KEY,
  embedding float[768]
);

-- Sync vectors
INSERT INTO vec_posts(id, embedding) SELECT id, embeddings FROM posts;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;We could also setup triggers to keep them up to date but in PocketBase I am using event hooks to keep the virtual table udpated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Generate the Recommendation&lt;/h2&gt;
&lt;p&gt;Now to generate the recommendation offline we just need to use one of the blog posts to use as the input query to then use &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html&quot;&gt;k-nearest neighbor search (kNN)&lt;/a&gt; to get N number of related posts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  vec_posts.id as id,
  vec_posts.embedding as embedding,
  posts.title as title,
  posts.description as description,
  posts.slug as slug
FROM vec_posts
INNER JOIN vec_posts.id = posts.id
WHERE embedding match ?
AND k = 6
ORDER BY distance;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We just need to provide the ? argument with the vector of the currently selected blog post, and then after we filter out the current blog post from the list then we have the N closest number of blog posts that are related in a vector database.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This makes it so no matter how many times a blog posts is visited no network calls are made for the recommendation which enables this to scale really well.&lt;/p&gt;
&lt;p&gt;To see this in action you can click around on the various blog posts I have on my site and see the generated descriptions and related posts at the end of each article.&lt;/p&gt;
</content:encoded></item><item><title>How To Host Your Podcast For Free On Github Pages</title><link>https://rodydavis.com/blog/podcast-github-pages/</link><guid isPermaLink="true">https://rodydavis.com/blog/podcast-github-pages/</guid><description>Host your podcast for free using GitHub Pages by forking and customizing a readily available repository, enabling website hosting and automated episode releases.</description><pubDate>Mon, 20 Jan 2025 01:07:48 GMT</pubDate><content:encoded>&lt;h1&gt;How To Host Your Podcast For Free On Github Pages&lt;/h1&gt;
&lt;p&gt;Do you have a story to tell and want to share it with the world but do not know where to start? Are you a developer looking to start a tech podcast? Are you looking to save money for hosting? Well this article is for you!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; You can fork &lt;a href=&quot;https://github.com/rodydavis/podcast-player&quot;&gt;this repo&lt;/a&gt; and customize it for your podcast!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/g_1_wz30qhrbqz.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pages.github.com/&quot;&gt;Github Pages&lt;/a&gt; allows you to host any website for free on Github. You can host any kind of file or content and will be distributed with Github CDN around the world. You can even setup your Podcast to release new content with &lt;a href=&quot;https://github.com/features/actions&quot;&gt;Github Actions&lt;/a&gt; for each new episode.&lt;/p&gt;
&lt;h2&gt;Create New Repo&lt;/h2&gt;
&lt;p&gt;If you already familiar with Github you can skip this step.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://help.github.com/en/enterprise/2.14/user/articles/creating-a-new-repository&quot;&gt;How to create a new Github repository.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Login or Create a free account for &lt;a href=&quot;https://github.com/&quot;&gt;Github&lt;/a&gt; and follow the instructions for creating the repo.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/g_2_om6lao7zki.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Clone the Repo&lt;/h2&gt;
&lt;p&gt;Now that you have the repo created on Github, download the project using Github Desktop.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://help.github.com/en/desktop/contributing-to-projects/cloning-a-repository-from-github-to-github-desktop&quot;&gt;How to clone a repository from Github.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After it finishes downloading you can open up the project in your favorite text editor. In this example we will be using VSCode.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;Download Visual Studio Code&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Create the Website&lt;/h2&gt;
&lt;p&gt;You can create the website with whatever tech stack you wish but in this example we will be using Flutter.&lt;/p&gt;
&lt;p&gt;You can skip this step if you do not want an online player.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://flutter.dev/&quot;&gt;Learn about Flutter.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Open up the project if you haven’t already with VSCode and open the terminal and type the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter create player
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the process finishes then you can edit the application UI. Make sure you have Flutter installed.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://flutter.dev/docs/get-started/install&quot;&gt;How to install Flutter.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Edit the website files to the following:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/rodydavis/podcast-player/tree/master/player&quot;&gt;Flutter podcast template.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Setting up Github Actions&lt;/h2&gt;
&lt;p&gt;Create a new Github Action that will release the new episode pushed to the master branch. If you chose not to have a podcast player and just want to host the files then you can add the audio files and the rss feed directly to the &lt;code&gt;gh-pages&lt;/code&gt; or &lt;code&gt;master&lt;/code&gt; branch and the files will be hosted instantly. Regardless make sure you have a file call &lt;code&gt;.nojekyll&lt;/code&gt; so the web deployment will be much faster.&lt;/p&gt;
&lt;h2&gt;Custom Domain&lt;/h2&gt;
&lt;p&gt;If you want to have your podcast hosted with a custom domain you can easily do this with Github pages. Follow this guide to set up your custom domain:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://help.github.com/en/github/working-with-github-pages/configuring-a-custom-domain-for-your-github-pages-site&quot;&gt;Custom domain on Github Pages.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Releasing new Content&lt;/h2&gt;
&lt;p&gt;When you have a new episode to release the steps are very simple. Make sure to export your audio file to mono and use mp3 format so it is smaller that 100mb otherwise you will need to set up Git LFS for the repo.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git-lfs.github.com/&quot;&gt;Setup Git LFS for Github repository.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Put the new mp3 audio file in the “player/web/audio” folder. Now edit the RSS feed which is located at “player/web/feed.xml” and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt;
&amp;lt;rss xmlns:googleplay=&amp;quot;http://www.google.com/schemas/play-podcasts/1.0&amp;quot; xmlns:itunes=&amp;quot;http://www.itunes.com/dtds/podcast-1.0.dtd&amp;quot; xmlns:atom=&amp;quot;http://www.w3.org/2005/Atom&amp;quot; xmlns:rawvoice=&amp;quot;http://www.rawvoice.com/rawvoiceRssModule/&amp;quot; xmlns:content=&amp;quot;http://purl.org/rss/1.0/modules/content/&amp;quot; version=&amp;quot;2.0&amp;quot;&amp;gt;
  &amp;lt;channel&amp;gt;
    &amp;lt;title&amp;gt;Creative Engineering&amp;lt;/title&amp;gt;
    &amp;lt;googleplay:author&amp;gt;Rody Davis, Norbert Kozsir&amp;lt;/googleplay:author&amp;gt;
    &amp;lt;rawvoice:rating&amp;gt;TV-G&amp;lt;/rawvoice:rating&amp;gt;
    &amp;lt;rawvoice:location&amp;gt;San Francisco, California&amp;lt;/rawvoice:location&amp;gt;
    &amp;lt;rawvoice:frequency&amp;gt;Weekly&amp;lt;/rawvoice:frequency&amp;gt;
    &amp;lt;author&amp;gt;Rody Davis, Norbert Kozsir&amp;lt;/author&amp;gt;
    &amp;lt;itunes:author&amp;gt;Rody Davis, Norbert Kozsir&amp;lt;/itunes:author&amp;gt;
    &amp;lt;itunes:email&amp;gt;rody.davis.jr@gmail.com&amp;lt;/itunes:email&amp;gt;
    &amp;lt;itunes:category text=&amp;quot;Technology&amp;quot; /&amp;gt;
    &amp;lt;image&amp;gt;
      &amp;lt;url&amp;gt;https://rodydavis.github.io/podcast-player/img/icon.webp&amp;lt;/url&amp;gt;
      &amp;lt;title&amp;gt;Creative Engineering&amp;lt;/title&amp;gt;
      &amp;lt;link&amp;gt;https://rodydavis.github.io/podcast-player/&amp;lt;/link&amp;gt;
    &amp;lt;/image&amp;gt;
    &amp;lt;itunes:owner&amp;gt;
      &amp;lt;itunes:name&amp;gt;Rody Davis&amp;lt;/itunes:name&amp;gt;
      &amp;lt;itunes:email&amp;gt;rody.davis.jr@gmail.com&amp;lt;/itunes:email&amp;gt;
    &amp;lt;/itunes:owner&amp;gt;
    &amp;lt;itunes:keywords&amp;gt;flutter,dart,github,vr,ar,web&amp;lt;/itunes:keywords&amp;gt;
    &amp;lt;copyright&amp;gt;Rody Davis Productions 2020&amp;lt;/copyright&amp;gt;
    &amp;lt;description&amp;gt;Exploring Flutter, VR, AR, Cross-Platform Projects&amp;lt;/description&amp;gt;
    &amp;lt;googleplay:image href=&amp;quot;https://rodydavis.github.io/podcast-player/img/icon.webp&amp;quot; /&amp;gt;
    &amp;lt;language&amp;gt;en-us&amp;lt;/language&amp;gt;
    &amp;lt;itunes:explicit&amp;gt;no&amp;lt;/itunes:explicit&amp;gt;
    &amp;lt;pubDate&amp;gt;Mon, 13 Apr 2020 13:00:00 PDT&amp;lt;/pubDate&amp;gt;
    &amp;lt;link&amp;gt;https://rodydavis.github.io/podcast-player/feed.xml&amp;lt;/link&amp;gt;
    &amp;lt;itunes:image href=&amp;quot;https://rodydavis.github.io/podcast-player/img/icon.webp&amp;quot; /&amp;gt;
    &amp;lt;item&amp;gt;
      &amp;lt;author&amp;gt;Rody Davis, Norbert Kozsir&amp;lt;/author&amp;gt;
      &amp;lt;itunes:author&amp;gt;Rody Davis, Norbert Kozsir&amp;lt;/itunes:author&amp;gt;
      &amp;lt;title&amp;gt;Flutter Desktop/Web and VR&amp;lt;/title&amp;gt;
      &amp;lt;pubDate&amp;gt;Mon, 13 Apr 2020 13:00:00 GMT&amp;lt;/pubDate&amp;gt;
      &amp;lt;enclosure url=&amp;quot;https://rodydavis.github.io/podcast-player/audio/01-create-eng.mp3&amp;quot; type=&amp;quot;audio/mpeg&amp;quot; length=&amp;quot;34216300&amp;quot; /&amp;gt;
      &amp;lt;itunes:duration&amp;gt;54:08&amp;lt;/itunes:duration&amp;gt;
      &amp;lt;guid isPermaLink=&amp;quot;false&amp;quot;&amp;gt;cepod01&amp;lt;/guid&amp;gt;
      &amp;lt;itunes:explicit&amp;gt;no&amp;lt;/itunes:explicit&amp;gt;
      &amp;lt;description&amp;gt;
Norbert Kozsir - @norbertkozsir

https://twitter.com/norbertkozsir

https://github.com/norbert515


Rody Davis - @rodydavis

https://twitter.com/rodydavis

https://github.com/rodydavis

https://youtube.com/rodydavis

https://rodydavis.com

Our podcast player: 

https://rodydavis.github.io/podcast-player/
        &amp;lt;/description&amp;gt;
    &amp;lt;/item&amp;gt;
  &amp;lt;/channel&amp;gt;
&amp;lt;/rss&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For every episode you just have to add a new item to the feed and change the info for the episode. I would suggest putting the new episodes at the bottom.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;item&amp;gt;        
    &amp;lt;author&amp;gt;COMMA\_SEPERATED\_LIST\_OF\_AUTHORS&amp;lt;/author&amp;gt;        
    &amp;lt;itunes:author&amp;gt;COMMA\_SEPERATED\_LIST\_OF\_AUTHORS&amp;lt;/itunes:author&amp;gt;      
    &amp;lt;title&amp;gt;PODCAST\_EPISODE\_TITLE&amp;lt;/title&amp;gt;        
    &amp;lt;pubDate&amp;gt;Mon, 13 Apr 2020 13:00:00 GMT&amp;lt;/pubDate&amp;gt;        
    &amp;lt;enclosure url=&amp;quot;LINK\_TO\_AUDIO\_FILE&amp;quot; type=&amp;quot;audio/mpeg&amp;quot; length=&amp;quot;34216300&amp;quot; /&amp;gt;        
    &amp;lt;itunes:duration&amp;gt;54:08&amp;lt;/itunes:duration&amp;gt;        
    &amp;lt;guid isPermaLink=&amp;quot;false&amp;quot;&amp;gt;cepod01&amp;lt;/guid&amp;gt;      
    &amp;lt;itunes:explicit&amp;gt;no&amp;lt;/itunes:explicit&amp;gt;        
    &amp;lt;description&amp;gt;  
    Show Notes Here!         
    &amp;lt;/description&amp;gt;      
&amp;lt;/item&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/n_2_kvxta7kwbn.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Publishing&lt;/h2&gt;
&lt;p&gt;Once your Github Action is finished building you now have an RSS feed that you can use to submit to Apple Podcasts, Google Podcasts and Spotify for Podcasters.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;https://GITHUB\_USERNAME.github.io/GITHUB\_REPO/feed.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also use this RSS Feed link to support any podcast player!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/g_3_v94djucpkr.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Hopefully you can see now how easy it is to host your podcast for free on Github Pages. You can find the final code for this example here:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/rodydavis/podcast-player&quot;&gt;Final Project.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Live Example&lt;/h2&gt;
&lt;p&gt;My “Creative Engineering” podcast is hosted using this technique:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://podcasts.apple.com/us/podcast/creative-engineering/id1507852833&quot;&gt;Apple Podcasts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://podcasts.google.com/feed/aHR0cHM6Ly9yb2R5ZGF2aXMuZ2l0aHViLmlvL2NyZWF0aXZlX2VuZ2luZWVyaW5nL2ZlZWQueG1s?ved=2ahUKEwiw5anO0dLqAhU2lZ4KHR3FDtcQ4aUDegQIARAC&amp;amp;hl=en-GB&quot;&gt;Google Podcasts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://open.spotify.com/show/3UTiK34aDOOSHFpGQ0RglN&quot;&gt;Spotify Podcasts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://music.amazon.com/podcasts/8884a5cb-a92a-4ba5-a3ef-906ac334386d/Creative-Engineering?ref=dm_wcp_pp_link_pr_s&quot;&gt;Amazon Music&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>How to do Bitwise operations in Dart</title><link>https://rodydavis.com/dart/bitwise/</link><guid isPermaLink="true">https://rodydavis.com/dart/bitwise/</guid><description>Learn how to perform bitwise operations (AND, OR, XOR, NAND, NOR) with integers and booleans in Dart, including examples and explanations.</description><pubDate>Sun, 19 Jan 2025 02:35:57 GMT</pubDate><content:encoded>&lt;h1&gt;How to do Bitwise operations in Dart&lt;/h1&gt;
&lt;p&gt;In Dart it is possible to do &lt;a href=&quot;https://en.wikipedia.org/wiki/Bitwise_operation#:~:text=In%20computer%20programming%2C%20a%20bitwise,directly%20supported%20by%20the%20processor.&quot;&gt;Bitwise Operations&lt;/a&gt; with &lt;strong&gt;int&lt;/strong&gt; and &lt;strong&gt;bool&lt;/strong&gt; types.&lt;/p&gt;
&lt;h2&gt;AND&lt;/h2&gt;
&lt;p&gt;Checks if the left and right side are both true. &lt;a href=&quot;https://www.ibm.com/docs/en/xl-c-and-cpp-aix/16.1?topic=expressions-bitwise-operator&quot;&gt;Learn more&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// int
print(0 &amp;amp; 1); // 0
print(1 &amp;amp; 0); // 0
print(1 &amp;amp; 1); // 1
print(0 &amp;amp; 0); // 0

// bool
print(false &amp;amp; true); // false
print(true &amp;amp; false); // false
print(true &amp;amp; true); // true
print(false &amp;amp; false); // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OR&lt;/h2&gt;
&lt;h3&gt;Inclusive&lt;/h3&gt;
&lt;p&gt;Checks if either the left or right side are true. &lt;a href=&quot;https://www.ibm.com/docs/en/xl-c-and-cpp-aix/16.1?topic=be-logical-operator&quot;&gt;Learn more&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// int
print(0 | 1); // 1
print(1 | 0); // 1
print(1 | 1); // 1
print(0 | 0); // 0

// bool
print(false | true); // true
print(true | false); // true
print(true | true); // true
print(false | false); // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Exclusive&lt;/h3&gt;
&lt;p&gt;Checks if both the left or right side are true but not both. &lt;a href=&quot;https://www.ibm.com/docs/en/xl-c-and-cpp-aix/16.1?topic=expressions-bitwise-exclusive-operator&quot;&gt;Learn more&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// int
print(0 ^ 1); // 1
print(1 ^ 0); // 1
print(1 ^ 1); // 0
print(0 ^ 0); // 0

// bool
print(false ^ true); // true
print(true ^ false); // true
print(true ^ true); // false
print(false ^ false); // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;NAND&lt;/h2&gt;
&lt;p&gt;Negated AND operation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// int
print(~(0 &amp;amp; 1) &amp;amp; 1); // 1
print(~(1 &amp;amp; 0) &amp;amp; 1); // 1
print(~(1 &amp;amp; 1) &amp;amp; 1); // 0
print(~(0 &amp;amp; 0) &amp;amp; 1); // 1

// bool
print(!(false &amp;amp; true)); // true
print(!(true &amp;amp; false)); // true
print(!(true &amp;amp; true)); // false
print(!(false &amp;amp; false)); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;NOR&lt;/h2&gt;
&lt;p&gt;Negated inclusive OR operation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// int
print(~(0 | 1) &amp;amp; 1); // 0
print(~(1 | 0) &amp;amp; 1); // 0
print(~(1 | 1) &amp;amp; 1); // 0
print(~(0 | 0) &amp;amp; 1); // 1

// bool
print(!(false | true)); // false
print(!(true | false)); // false
print(!(true | true)); // false
print(!(false | false)); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;XNOR&lt;/h2&gt;
&lt;p&gt;Negated exclusive OR operation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// int
print(~(0 ^ 1) &amp;amp; 1); // 0
print(~(1 ^ 0) &amp;amp; 1); // 0
print(~(1 ^ 1) &amp;amp; 1); // 1
print(~(0 ^ 0) &amp;amp; 1); // 1

// bool
print(!(false ^ true)); // false
print(!(true ^ false)); // false
print(!(true ^ true)); // true
print(!(false ^ false)); // true
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>How to Run Astro SSR and PocketBase on the Same Server</title><link>https://rodydavis.com/astro/ssr-pocketbase/</link><guid isPermaLink="true">https://rodydavis.com/astro/ssr-pocketbase/</guid><description>This tutorial demonstrates how to run Astro with server-side rendering (SSR) and PocketBase on the same server by setting up a Go proxy to route requests between them.</description><pubDate>Sat, 18 Jan 2025 22:05:53 GMT</pubDate><content:encoded>&lt;h1&gt;How to Run Astro SSR and PocketBase on the Same Server&lt;/h1&gt;
&lt;p&gt;In this article I will show you how to host &lt;a href=&quot;https://pocketbase.io/&quot;&gt;PocketBase&lt;/a&gt; and &lt;a href=&quot;https://docs.astro.build/en/guides/server-side-rendering/&quot;&gt;Astro in SSR&lt;/a&gt; mode on the same server. PocketBase does let you &lt;a href=&quot;https://pocketbase.io/docs/go-rendering-templates/&quot;&gt;render templates&lt;/a&gt; on the server but requires &lt;a href=&quot;https://pkg.go.dev/text/template&quot;&gt;Go Templates&lt;/a&gt; or pre-building with Static Site Generation (SSG).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This could also be modified to use your web server or framework of choice (&lt;a href=&quot;https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering&quot;&gt;Next.js&lt;/a&gt;, &lt;a href=&quot;https://kit.svelte.dev/docs/page-options&quot;&gt;SvelteKit&lt;/a&gt;, &lt;a href=&quot;https://qwik.builder.io/&quot;&gt;Qwik&lt;/a&gt;, &lt;a href=&quot;https://angular.io/guide/ssr&quot;&gt;Angular&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Before getting started make sure you have the latest version of &lt;a href=&quot;https://nodejs.org/en/blog/announcements/v19-release-announce&quot;&gt;Node&lt;/a&gt; and &lt;a href=&quot;https://go.dev/doc/install&quot;&gt;Go&lt;/a&gt; installed locally.&lt;/p&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;In a terminal run the following to create the base project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir pocketbase_astro_ssr
cd pocketbase_astro_ssr
mkdir server
mkdir www
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create the &lt;code&gt;server&lt;/code&gt; and &lt;code&gt;www&lt;/code&gt; folders in our project needed for both Astro and PocketBase.&lt;/p&gt;
&lt;h2&gt;Setting up the server&lt;/h2&gt;
&lt;p&gt;Create a file at &lt;code&gt;server/main.go&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;package main

import (
	&amp;quot;log&amp;quot;
	&amp;quot;net/http/httputil&amp;quot;
	&amp;quot;net/url&amp;quot;

	&amp;quot;github.com/labstack/echo/v5&amp;quot;
	&amp;quot;github.com/pocketbase/pocketbase&amp;quot;
	&amp;quot;github.com/pocketbase/pocketbase/core&amp;quot;
)

func main() {
    app := pocketbase.New()

    app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
        proxy := httputil.NewSingleHostReverseProxy(&amp;amp;url.URL{
			Scheme: &amp;quot;http&amp;quot;,
			Host:   &amp;quot;localhost:4321&amp;quot;,
		})
		e.Router.Any(&amp;quot;/*&amp;quot;, echo.WrapHandler(proxy))
		e.Router.Any(&amp;quot;/&amp;quot;, echo.WrapHandler(proxy))
        return nil
    })

    if err := app.Start(); err != nil {
        log.Fatal(err)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are extending &lt;a href=&quot;https://pocketbase.io/docs/go-overview/&quot;&gt;PocketBase with Go&lt;/a&gt; and taking advantage of the &lt;a href=&quot;https://echo.labstack.com/docs/routing&quot;&gt;Echo router&lt;/a&gt; integration and using a &lt;a href=&quot;https://www.nginx.com/resources/glossary/reverse-proxy-server/#:~:text=A%20reverse%20proxy%20server%20is,traffic%20between%20clients%20and%20servers.&quot;&gt;reverse proxy&lt;/a&gt; to handle all requests not defined by PocketBase already and delegating them to Astro.&lt;/p&gt;
&lt;p&gt;Next run the following in a terminal to install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;go mod init server
go mod tidy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can start the server and move on to the client:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;go run main.go serve
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see the following and note that this will run in debug mode so all the SQL statements will start to show:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;2023/11/09 10:28:52 Server started at http://127.0.0.1:8090
├─ REST API: http://127.0.0.1:8090/api/
└─ Admin UI: http://127.0.0.1:8090/_/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Collections&lt;/h3&gt;
&lt;p&gt;Open up the Admin UI url and after creating a new admin user, create a new collection &lt;code&gt;items&lt;/code&gt; and add the following metadata:&lt;/p&gt;
&lt;p&gt;Column Name&lt;/p&gt;
&lt;p&gt;Column Type&lt;/p&gt;
&lt;p&gt;Column Settings&lt;/p&gt;
&lt;p&gt;title&lt;/p&gt;
&lt;p&gt;Plain Text&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/astro_ssr_1_l8se0qq5gx.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then update the API Rules to allow read access for list and view.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/astro_ssr_2_nbohwwy3lp.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is just for example purposes and on a production app you will rely on auth for ACLs&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Create 3 new records with placeholder data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/astro_ssr_3_s8uxkyvvla.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Creating the client&lt;/h2&gt;
&lt;p&gt;Now we can create the client that will be used to connect to PocketBase and serve all of the web traffic.&lt;/p&gt;
&lt;p&gt;Navigate to the &lt;code&gt;www&lt;/code&gt; directory and run the following in a terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm create astro@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Follow the prompts and enter the following:&lt;/p&gt;
&lt;p&gt;Question&lt;/p&gt;
&lt;p&gt;Answer&lt;/p&gt;
&lt;p&gt;Where should we create your new project?&lt;/p&gt;
&lt;p&gt;.&lt;/p&gt;
&lt;p&gt;How would you like to start your new project?&lt;/p&gt;
&lt;p&gt;Empty&lt;/p&gt;
&lt;p&gt;Install dependencies?&lt;/p&gt;
&lt;p&gt;Yes&lt;/p&gt;
&lt;p&gt;Do you plan to write TypeScript?&lt;/p&gt;
&lt;p&gt;Yes&lt;/p&gt;
&lt;p&gt;How strict should TypeScript be?&lt;/p&gt;
&lt;p&gt;Strict&lt;/p&gt;
&lt;p&gt;Initialize a new git repository?&lt;/p&gt;
&lt;p&gt;No&lt;/p&gt;
&lt;p&gt;You can of course customize this as you need, but next we can install the dependencies needed by running the following in a terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm i -D @astrojs/node
npm i pocketbase
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next update &lt;code&gt;www/astro.config.mjs&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import { defineConfig } from &amp;quot;astro/config&amp;quot;;
import nodejs from &amp;quot;@astrojs/node&amp;quot;;

// https://astro.build/config
export default defineConfig({
  adapter: nodejs({
    mode: &amp;quot;standalone&amp;quot;,
  }),
  output: &amp;quot;server&amp;quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will use Server Side Rendering (SSR) instead of Static Site Generation (SSG) when we run the web server.&lt;/p&gt;
&lt;h3&gt;UI&lt;/h3&gt;
&lt;h4&gt;Layouts&lt;/h4&gt;
&lt;p&gt;We can start by creating a shared layout for all the routes. Create a file at &lt;code&gt;www/src/layouts/Root.astro&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;---
interface Props {
  title: string;
}

const { title } = Astro.props;
---

&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;utf-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;generator&amp;quot; content={Astro.generator} /&amp;gt;
    &amp;lt;title&amp;gt;{title}&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;slot /&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Routes&lt;/h4&gt;
&lt;p&gt;Now we can update the index &lt;code&gt;/&lt;/code&gt; route by updating the following file &lt;code&gt;www/src/pages/index.astro&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;---
import Root from &amp;quot;../layouts/Root.astro&amp;quot;;

import PocketBase from &amp;quot;pocketbase&amp;quot;;

const pb = new PocketBase(&amp;quot;http://127.0.0.1:8090&amp;quot;);
const items = pb.collection(&amp;quot;items&amp;quot;);
const records = await items.getFullList();
---

&amp;lt;Root title=&amp;quot;Items&amp;quot;&amp;gt;
  &amp;lt;h1&amp;gt;Items&amp;lt;/h1&amp;gt;
  &amp;lt;ul&amp;gt;
    {
      records.map((record) =&amp;gt; (
        &amp;lt;li&amp;gt;
          &amp;lt;a href={`/items/${record.id}`}&amp;gt;{record.title}&amp;lt;/a&amp;gt;
        &amp;lt;/li&amp;gt;
      ))
    }
  &amp;lt;/ul&amp;gt;
&amp;lt;/Root&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will call the &lt;code&gt;items&lt;/code&gt; collection on the server and render it with 0 JS on the client.&lt;/p&gt;
&lt;p&gt;Next create a file &lt;code&gt;www/src/pages/[...slug].astro&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;---
import Root from &amp;quot;../layouts/Root.astro&amp;quot;;

import PocketBase from &amp;quot;pocketbase&amp;quot;;

const slug = Astro.params.slug!;
const id = slug.split(&amp;quot;/&amp;quot;).pop()!;

const pb = new PocketBase(&amp;quot;http://127.0.0.1:8090&amp;quot;);
const items = pb.collection(&amp;quot;items&amp;quot;);

const records = await items.getList(1, 1, {
  filter: `id = &apos;${id}&apos;`,
});

if (records.items.length === 0) {
  return new Response(&amp;quot;Not found&amp;quot;, { status: 404 });
}

const {title} = records.items[0];
---

&amp;lt;Root {title}&amp;gt;
  &amp;lt;a href=&amp;quot;/&amp;quot;&amp;gt;Back&amp;lt;/a&amp;gt;
  &amp;lt;h1&amp;gt;{title}&amp;lt;/h1&amp;gt;
&amp;lt;/Root&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is almost like before but now we can return a proper &lt;code&gt;404&lt;/code&gt; response if not found for an item.&lt;/p&gt;
&lt;h4&gt;Running&lt;/h4&gt;
&lt;p&gt;Now we can run the web server with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;gt; dev
&amp;gt; astro dev

  🚀  astro  v3.4.4 started in 67ms
  
  ┃ Local    http://localhost:4321/
  ┃ Network  use --host to expose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then if we open up the PocketBase url &lt;code&gt;http://127.0.0.1:8090&lt;/code&gt; and you should see the following for the index route and detail routes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/astro_ssr_4_2jgteusxtt.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/astro_ssr_5_3u7bekhf36.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Now you can build a new binary for both the server and client and deploy them both on the same server instance. 🎉&lt;/p&gt;
&lt;p&gt;You can find the final code &lt;a href=&quot;https://github.com/rodydavis/pocketbase_astro_ssr&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>How to Build a WebRTC Signal Server with PocketBase</title><link>https://rodydavis.com/backend/pocketbase-webrtc/</link><guid isPermaLink="true">https://rodydavis.com/backend/pocketbase-webrtc/</guid><description>Build a WebRTC signal server using PocketBase, a lightweight backend with SQLite, Server Sent Events, and an easy-to-use admin UI.</description><pubDate>Sat, 18 Jan 2025 22:32:05 GMT</pubDate><content:encoded>&lt;h1&gt;How to Build a WebRTC Signal Server with PocketBase&lt;/h1&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;If you are new to WebRTC then I suggest checking out this great Fireship video on &lt;a href=&quot;https://youtu.be/WmR9IMUD_CY?si=c6xEDVslDOsIJzyP&quot;&gt;WebRTC in 100 seconds&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;Also if you are looking for a &lt;a href=&quot;https://firebase.google.com/&quot;&gt;Firebase&lt;/a&gt; example then check out &lt;a href=&quot;https://github.com/fireship-io/webrtc-firebase-demo&quot;&gt;this repository&lt;/a&gt; which this example is largely based on.&lt;/p&gt;
&lt;p&gt;This example is built using &lt;a href=&quot;https://pocketbase.io/&quot;&gt;PocketBase&lt;/a&gt; as the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling&quot;&gt;signal server&lt;/a&gt; for &lt;a href=&quot;https://webrtc.org/&quot;&gt;WebRTC&lt;/a&gt; and runs &lt;a href=&quot;https://www.sqlite.org/index.html&quot;&gt;SQLite&lt;/a&gt; on the server with easy to use realtime SDKs built on top of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events&quot;&gt;Server Sent Events (SSE)&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Setting up the server&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://pocketbase.io/docs/&quot;&gt;Download PocketBase&lt;/a&gt; and create a new directory that we will use for the project.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir webrtc-pocketbase-demo
cd webrtc-pocketbase-demo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy the PocketBase binary into the directory you just created under a sub directory &lt;code&gt;.pb&lt;/code&gt;. If you are on MacOS you will need to &lt;a href=&quot;https://discussions.apple.com/thread/253681758&quot;&gt;allow the executable&lt;/a&gt; to run in settings.&lt;/p&gt;
&lt;p&gt;Start the PocketBase server with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;.pb/pocketbase serve
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If all goes well you should see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;2023/11/04 15:10:56 Server started at http://127.0.0.1:8090
├─ REST API: http://127.0.0.1:8090/api/
└─ Admin UI: http://127.0.0.1:8090/_/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open up the Admin UI url and create a new username and password.&lt;/p&gt;
&lt;p&gt;For this example the email and password will be the following:&lt;/p&gt;
&lt;p&gt;Key&lt;/p&gt;
&lt;p&gt;Value&lt;/p&gt;
&lt;p&gt;Email&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;mailto:test@example.com&quot;&gt;test@example.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Password&lt;/p&gt;
&lt;p&gt;Test123456789&lt;/p&gt;
&lt;p&gt;You should now see the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_1_sxepcsqwya.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Creating the collections&lt;/h3&gt;
&lt;h4&gt;ice_servers&lt;/h4&gt;
&lt;p&gt;Create a new collection named &lt;code&gt;ice_servers&lt;/code&gt; with the following columns:&lt;/p&gt;
&lt;p&gt;Column Name&lt;/p&gt;
&lt;p&gt;Column Type&lt;/p&gt;
&lt;p&gt;url&lt;/p&gt;
&lt;p&gt;Plain text&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_2_18muflf746.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Add the following API rule to the List/Search and View:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;@request.auth.id != &apos;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_3_zhpjwks4he.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;After the collection is created add 2 records for each of the following values for the url:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;stun:stun1.l.google.com:19302
stun:stun2.l.google.com:19302
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_4_u30xyj67qq.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;calls&lt;/h4&gt;
&lt;p&gt;Create a new collection named &lt;code&gt;calls&lt;/code&gt; with the following columns:&lt;/p&gt;
&lt;p&gt;Column Name&lt;/p&gt;
&lt;p&gt;Column Type&lt;/p&gt;
&lt;p&gt;Column Settings&lt;/p&gt;
&lt;p&gt;user_id&lt;/p&gt;
&lt;p&gt;Relation&lt;/p&gt;
&lt;p&gt;Non empty, &lt;code&gt;users&lt;/code&gt;, Cascade delete is &lt;code&gt;true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;offer&lt;/p&gt;
&lt;p&gt;JSON&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;answer&lt;/p&gt;
&lt;p&gt;JSON&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_5_3x9zuluckq.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;it is also possible to limit the user to one call each by setting the Unique constraint on the &lt;code&gt;user_id&lt;/code&gt; column.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_6_epid0nfuym.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Add the following API rule to all of the methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;@request.auth.id != &apos;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_7_6pyvjpk8jx.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;offer_candidates&lt;/h4&gt;
&lt;p&gt;Create a new collection named &lt;code&gt;offer_candidates&lt;/code&gt; with the following columns:&lt;/p&gt;
&lt;p&gt;Column Name&lt;/p&gt;
&lt;p&gt;Column Type&lt;/p&gt;
&lt;p&gt;Column Settings&lt;/p&gt;
&lt;p&gt;call_id&lt;/p&gt;
&lt;p&gt;Relation&lt;/p&gt;
&lt;p&gt;Non empty, &lt;code&gt;calls&lt;/code&gt;, Cascade delete is &lt;code&gt;true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;data&lt;/p&gt;
&lt;p&gt;JSON&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_8_c1ecscf02n.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Add the following API rule to all of the methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;@request.auth.id != &apos;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;answer_candidates&lt;/h4&gt;
&lt;p&gt;Create a new collection named &lt;code&gt;answer_candidates&lt;/code&gt; with the following columns:&lt;/p&gt;
&lt;p&gt;Column Name&lt;/p&gt;
&lt;p&gt;Column Type&lt;/p&gt;
&lt;p&gt;Column Settings&lt;/p&gt;
&lt;p&gt;call_id&lt;/p&gt;
&lt;p&gt;Relation&lt;/p&gt;
&lt;p&gt;Non empty, &lt;code&gt;calls&lt;/code&gt;, Cascade delete is &lt;code&gt;true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;data&lt;/p&gt;
&lt;p&gt;JSON&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_9_8urvznju5m.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Add the following API rule to all of the methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;@request.auth.id != &apos;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_10_rj34gi9mwc.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;users&lt;/h4&gt;
&lt;p&gt;For demo purposes we will not be including an auth form for the user, but to make the example simple create a new user with the same login info for the admin.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_11_a359eqpxdy.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_12_m6rwgbmoum.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Setting up the client&lt;/h2&gt;
&lt;p&gt;Navigate to the directory and run the following commands to get started:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init -y
npm i -D vite
npm i pocketbase
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;package.json&lt;/code&gt; to be the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &amp;quot;name&amp;quot;: &amp;quot;webrtc-pocketbase-demo&amp;quot;,
  &amp;quot;version&amp;quot;: &amp;quot;0.0.0&amp;quot;,
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;dev&amp;quot;: &amp;quot;vite&amp;quot;,
    &amp;quot;build&amp;quot;: &amp;quot;vite build&amp;quot;,
    &amp;quot;serve&amp;quot;: &amp;quot;vite preview&amp;quot;
  },
  &amp;quot;devDependencies&amp;quot;: {
    &amp;quot;vite&amp;quot;: &amp;quot;^4.5.0&amp;quot;
  },
  &amp;quot;dependencies&amp;quot;: {
    &amp;quot;pocketbase&amp;quot;: &amp;quot;^0.19.0&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are in a Git repository update/create the &lt;code&gt;.gitignore&lt;/code&gt; to have the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;node_modules
.DS_Store
dist
dist-ssr
*.local
.pb
.env
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HTML&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;index.html&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;WebRTC Pocketbase Demo&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h2&amp;gt;1. Start your Webcam&amp;lt;/h2&amp;gt;
    &amp;lt;div class=&amp;quot;videos&amp;quot;&amp;gt;
      &amp;lt;span&amp;gt;
        &amp;lt;h3&amp;gt;Local Stream&amp;lt;/h3&amp;gt;
        &amp;lt;video id=&amp;quot;webcamVideo&amp;quot; autoplay playsinline&amp;gt;&amp;lt;/video&amp;gt;
      &amp;lt;/span&amp;gt;
      &amp;lt;span&amp;gt;
        &amp;lt;h3&amp;gt;Remote Stream&amp;lt;/h3&amp;gt;
        &amp;lt;video id=&amp;quot;remoteVideo&amp;quot; autoplay playsinline&amp;gt;&amp;lt;/video&amp;gt;
      &amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;button id=&amp;quot;webcamButton&amp;quot;&amp;gt;Start webcam&amp;lt;/button&amp;gt;
    &amp;lt;h2&amp;gt;2. Create a new Call&amp;lt;/h2&amp;gt;
    &amp;lt;button id=&amp;quot;callButton&amp;quot; disabled&amp;gt;Create Call (offer)&amp;lt;/button&amp;gt;``
    &amp;lt;h2&amp;gt;3. Join a Call&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;Answer the call from a different browser window or device&amp;lt;/p&amp;gt;
    &amp;lt;input id=&amp;quot;callInput&amp;quot; /&amp;gt;
    &amp;lt;button id=&amp;quot;answerButton&amp;quot; disabled&amp;gt;Answer&amp;lt;/button&amp;gt;
    &amp;lt;h2&amp;gt;4. Hangup&amp;lt;/h2&amp;gt;
    &amp;lt;button id=&amp;quot;hangupButton&amp;quot; disabled&amp;gt;Hangup&amp;lt;/button&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/main.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CSS&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;style.css&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;body {
  --text-color: #2c3e50;
  --video-background-color: #2c3e50;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, &amp;quot;Segoe UI&amp;quot;, Roboto,
    Oxygen, Ubuntu, Cantarell, &amp;quot;Open Sans&amp;quot;, &amp;quot;Helvetica Neue&amp;quot;, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: var(--text-color);
  margin: 80px 10px;
}

video {
  width: 40vw;
  height: 30vw;
  margin: 2rem;
  background: var(--video-background-color);
}

.videos {
  display: flex;
  align-items: center;
  justify-content: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JS&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;main.js&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import &amp;quot;./style.css&amp;quot;;

import PocketBase from &amp;quot;pocketbase&amp;quot;;

const pb = new PocketBase(&amp;quot;http://127.0.0.1:8090&amp;quot;);

const calls = pb.collection(&amp;quot;calls&amp;quot;);
const offerCandidates = pb.collection(&amp;quot;offer_candidates&amp;quot;);
const answerCandidates = pb.collection(&amp;quot;answer_candidates&amp;quot;);

const webcamButton = document.getElementById(&amp;quot;webcamButton&amp;quot;);
const webcamVideo = document.getElementById(&amp;quot;webcamVideo&amp;quot;);
const callButton = document.getElementById(&amp;quot;callButton&amp;quot;);
const callInput = document.getElementById(&amp;quot;callInput&amp;quot;);
const answerButton = document.getElementById(&amp;quot;answerButton&amp;quot;);
const remoteVideo = document.getElementById(&amp;quot;remoteVideo&amp;quot;);
const hangupButton = document.getElementById(&amp;quot;hangupButton&amp;quot;);

const auth = await pb
  .collection(&amp;quot;users&amp;quot;)
  .authWithPassword(
    import.meta.env.VITE_POCKETBASE_USERNAME,
    import.meta.env.VITE_POCKETBASE_PASSWORD
  );
const userId = auth.record.id;
const iceServers = await pb.collection(&amp;quot;ice_servers&amp;quot;).getFullList();

const servers = {
  iceServers: [{ urls: iceServers.map((e) =&amp;gt; e.url) }],
  iceCandidatePoolSize: 10,
};

const pc = new RTCPeerConnection(servers);
let localStream = null;
let remoteStream = null;

webcamButton.onclick = async () =&amp;gt; {
  localStream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true,
  });
  remoteStream = new MediaStream();

  localStream.getTracks().forEach((track) =&amp;gt; {
    pc.addTrack(track, localStream);
  });

  pc.ontrack = (event) =&amp;gt; {
    const stream = event.streams[0];
    stream.getTracks().forEach((track) =&amp;gt; {
      remoteStream.addTrack(track);
    });
  };

  webcamVideo.srcObject = localStream;
  remoteVideo.srcObject = remoteStream;

  callButton.disabled = false;
  answerButton.disabled = false;
  webcamButton.disabled = true;
};

callButton.onclick = async () =&amp;gt; {
  const call = await calls.create({
    user_id: userId,
  });
  const callId = call.id;

  callInput.value = callId;

  pc.onicecandidate = (event) =&amp;gt; {
    event.candidate &amp;amp;&amp;amp;
      offerCandidates.create({
        call_id: callId,
        data: event.candidate.toJSON(),
      });
  };

  const offerDescription = await pc.createOffer();
  await pc.setLocalDescription(offerDescription);

  const offer = {
    sdp: offerDescription.sdp,
    type: offerDescription.type,
  };

  await calls.update(callId, { offer });

  calls.subscribe(callId, (e) =&amp;gt; {
    const data = e.record;
    if (!pc.currentRemoteDescription &amp;amp;&amp;amp; data?.answer) {
      const answerDescription = new RTCSessionDescription(data.answer);
      pc.setRemoteDescription(answerDescription);
    }
  });

  answerCandidates.subscribe(&amp;quot;*&amp;quot;, (e) =&amp;gt; {
    if (e.action === &amp;quot;create&amp;quot;) {
      if (e.record?.call_id === callId) {
        const data = e.record.data;
        const candidate = new RTCIceCandidate(data);
        pc.addIceCandidate(candidate);
      }
    }
  });

  hangupButton.disabled = false;
};

answerButton.onclick = async () =&amp;gt; {
  const callId = callInput.value;
  const call = await calls.getOne(callId);

  pc.onicecandidate = (event) =&amp;gt; {
    event.candidate &amp;amp;&amp;amp;
      answerCandidates.create({
        call_id: call.id,
        data: event.candidate.toJSON(),
      });
  };

  const offerDescription = call.offer;
  const remoteDescription = new RTCSessionDescription(offerDescription);
  await pc.setRemoteDescription(remoteDescription);

  const answerDescription = await pc.createAnswer();
  await pc.setLocalDescription(answerDescription);

  const answer = {
    type: answerDescription.type,
    sdp: answerDescription.sdp,
  };

  await calls.update(call.id, { answer });

  offerCandidates.subscribe(&amp;quot;*&amp;quot;, async (e) =&amp;gt; {
    if (e.record?.call_id === call.id) {
      if (e.action === &amp;quot;create&amp;quot;) {
        const data = e.record.data;
        const candidate = new RTCIceCandidate(data);
        await pc.addIceCandidate(candidate);
      } else if (e.action === &amp;quot;delete&amp;quot;) {
        await offerCandidates.unsubscribe();
        window.location.reload();
      }
    }
  });
};

hangupButton.onclick = async () =&amp;gt; {
  const callId = callInput.value;
  pc.close();
  await calls.unsubscribe(callId);
  await calls.delete(callId);
  await answerCandidates.unsubscribe();
  window.location.reload();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Running the example&lt;/h2&gt;
&lt;p&gt;Run the following command to start the client (make sure the server is running in a separate terminal client):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If successful you should see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;  VITE v4.5.0  ready in 547 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open up two browsers with the same url:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_13_9b6nwkhq3i.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the first window click &lt;code&gt;Start webcam&lt;/code&gt; and then &lt;code&gt;Create Call (offer)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This will ask for camera permission and then generate a new id and add it to the &lt;code&gt;Join a Call&lt;/code&gt; text field.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_14_rat2efjyy3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Copy the new id and paste it in the second window field and click &lt;code&gt;Start webcam&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/web_rtc_15_438n00ql98.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then click &lt;code&gt;Hangup&lt;/code&gt; when you are done with the call 🎉.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;You can find the source code &lt;a href=&quot;https://github.com/rodydavis/webrtc-pocketbase-demo&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>How to Export SQLite Tables to CREATE Statements</title><link>https://rodydavis.com/dart/export-sqlite/</link><guid isPermaLink="true">https://rodydavis.com/dart/export-sqlite/</guid><description>Export SQLite database schema to CREATE statements using Dart, SQLite3, and Mustache templating for code generation.</description><pubDate>Sat, 18 Jan 2025 20:43:35 GMT</pubDate><content:encoded>&lt;h1&gt;How to Export SQLite Tables to CREATE Statements&lt;/h1&gt;
&lt;p&gt;In this article I will show you how to export all the tables and indexes in a &lt;a href=&quot;https://www.sqlite.org/index.html&quot;&gt;SQLite&lt;/a&gt; database to CREATE statements at runtime.&lt;/p&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Start by creating a new directory and &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt; project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir sqlite_introspect
cd sqlite_introspect
flutter create .
flutter pub add sqlite3 mustache_template
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will add the &lt;code&gt;sqlite3&lt;/code&gt; package which uses FFI to call the native executable and mustache that we will use for templates later.&lt;/p&gt;
&lt;h2&gt;Creating the database&lt;/h2&gt;
&lt;p&gt;Creating the database can be done either in memory or based on a local file. For this example we will use in memory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final Database db = sqlite3.openInMemory();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don&apos;t forget to dispose of the database after use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;db.dispose();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Defining the template&lt;/h2&gt;
&lt;p&gt;Since we will be using &lt;a href=&quot;https://mustache.github.io/&quot;&gt;Mustache&lt;/a&gt; we can define the variables that we will pass to the template as JSON.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;TableInfo&lt;/code&gt; class that will store the fields and indexes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class TableInfo {
  final String name;
  final List&amp;lt;Map&amp;lt;String, dynamic&amp;gt;&amp;gt; fields;
  final List&amp;lt;Map&amp;lt;String, dynamic&amp;gt;&amp;gt; indexes;

  TableInfo({
    required this.name,
    required this.fields,
    required this.indexes,
  });

  Map&amp;lt;String, dynamic&amp;gt; toJson() {
    return {
      &apos;name&apos;: name,
      &apos;fields&apos;: [
        for (var i = 0; i &amp;lt; fields.length; i++)
          {
            &apos;index&apos;: i,
            &apos;table&apos;: name,
            &apos;isLast&apos;: i == fields.length - 1,
            ...fields[i],
          },
      ],
      &apos;indexes&apos;: [
        for (var i = 0; i &amp;lt; indexes.length; i++)
          {
            &apos;index&apos;: i,
            &apos;table&apos;: name,
            &apos;isLast&apos;: i == indexes.length - 1,
            ...indexes[i],
          },
      ],
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can create the Mustache template used to build up the CREATE statements:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;const template = &apos;&apos;&apos;
{{#tables}}
CREATE TABLE {{name}} (
  {{#fields}}
  {{name}} {{#type}} {{.}}{{/type}}{{#notnull}} NOT NULL{{/notnull}}{{#pk}} PRIMARY KEY{{/pk}}{{#dflt_value}} DEFAULT {{.}}{{/dflt_value}}{{^isLast}},{{/isLast}}
  {{/fields}}
);
{{#indexes}}
CREATE {{#unique}} UNIQUE{{/unique}} {{name}}
ON {{table}}({{#values}} {{name}} {{/values}}{{^isLast}},{{/isLast}});
{{/indexes}}
{{/tables}}
&apos;&apos;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Exporting the PRAGMA&lt;/h2&gt;
&lt;p&gt;Now we can export the &lt;a href=&quot;https://www.sqlite.org/pragma.html&quot;&gt;PRAGMA&lt;/a&gt; for the database by exporting the list of tables, querying the column information and indexes about each one.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final tables = &amp;lt;TableInfo&amp;gt;[];
// Export table names
final tableNames = db
	.select(&amp;quot;SELECT name FROM sqlite_master WHERE type=&apos;table&apos;;&amp;quot;)
	.map((e) =&amp;gt; e[&apos;name&apos;] as String);
for (final t in tableNames) {
  // Export column information
  final info = db.select(&apos;PRAGMA table_info($t);&apos;);
  final tbl = TableInfo(name: t, fields: [], indexes: []);
  for (final c in info) {
    tbl.fields.add(c);
  }
  // Export index names
  final indexList = db.select(&apos;PRAGMA index_list($t);&apos;);
  for (final index in indexList) {
    final name = index[&apos;name&apos;] as String;
    // Export index information
    final infos = db.select(&apos;PRAGMA index_info($name);&apos;);
    final indexValue = {...index, &apos;values&apos;: infos};
    tbl.indexes.add(indexValue);
  }
  tables.add(tbl);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rendering the template&lt;/h2&gt;
&lt;p&gt;Now take the tables we just exported and pass them to the mustache template to render:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final tml = Template(template);
final args = {&amp;quot;tables&amp;quot;: tables.map((e) =&amp;gt; e.toJson()).toList()};
final str = tml.renderString(args);
print(str);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will now print out all the tables and indexes as CREATE as valid SQL. 🎉&lt;/p&gt;
</content:encoded></item><item><title>Various Ways to Invoke Functions in Dart</title><link>https://rodydavis.com/dart/function-invoking/</link><guid isPermaLink="true">https://rodydavis.com/dart/function-invoking/</guid><description>Dart functions can be invoked using positional arguments, named arguments in any order, the `.call` operator, or `Function.apply` for dynamic invocation, with `Function.apply` impacting compilation size and performance.</description><pubDate>Sun, 19 Jan 2025 02:43:50 GMT</pubDate><content:encoded>&lt;h1&gt;Various Ways to Invoke Functions in Dart&lt;/h1&gt;
&lt;p&gt;There are multiple ways to call a &lt;a href=&quot;https://dart.dev/language/functions&quot;&gt;Function&lt;/a&gt; in Dart.&lt;/p&gt;
&lt;p&gt;The examples below will assume the following function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;void myFunction(int a, int b, {int? c, int? d}) {
  print((a, b, c, d));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But recently I learned that you can call a functions positional arguments in any order mixed with the named arguments. 🤯&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;myFunction(1, 2, c: 3, d: 4);
myFunction(1, c: 3, d: 4, 2);
myFunction(c: 3, d: 4, 1, 2);
myFunction(c: 3, 1, 2, d: 4);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In addition you can use the &lt;a href=&quot;https://dart.dev/language/callable-objects&quot;&gt;&lt;code&gt;.call&lt;/code&gt;&lt;/a&gt; operator to invoke the function if you have a reference to it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;myFunction.call(1, 2, c: 3, d: 4);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also use &lt;a href=&quot;https://api.flutter.dev/flutter/dart-core/Function/apply.html&quot;&gt;&lt;code&gt;Function.apply&lt;/code&gt;&lt;/a&gt; to dynamically invoke a function with a reference but it should be noted that it will effect js dart complication size and performance:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;Function.apply(myFunction, [1, 2], {#c: 3, #d: 4});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All of these methods print the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;(1, 2, 3, 4)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
</content:encoded></item><item><title>How to create HTML Web Components with Dart</title><link>https://rodydavis.com/dart/html-web-components/</link><guid isPermaLink="true">https://rodydavis.com/dart/html-web-components/</guid><description>Learn how to build reusable HTML web components with Dart, leveraging their benefits for framework-agnostic development and progressive enhancement.</description><pubDate>Fri, 14 Feb 2025 12:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to create HTML Web Components with Dart&lt;/h1&gt;
&lt;p&gt;I am a long time &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_components&quot;&gt;Web Components&lt;/a&gt; fan (since helping DevRel &lt;a href=&quot;https://lit.dev/&quot;&gt;lit.dev&lt;/a&gt; and &lt;a href=&quot;https://github.com/material-components/material-web&quot;&gt;Material Web Components&lt;/a&gt;) and have also loved writing &lt;a href=&quot;https://dart.dev/&quot;&gt;Dart&lt;/a&gt; in both &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt; applications and full stack apps.&lt;/p&gt;
&lt;p&gt;Despite being &lt;a href=&quot;https://arewebcomponentsathingyet.com/&quot;&gt;used at so many companies&lt;/a&gt;, Web Components have faced a lot of pushback from JavaScript developers that use frameworks to target the web. ☹️&lt;/p&gt;
&lt;p&gt;What you may not realize is that the web has a way to create new HTML tags that can be used in &lt;strong&gt;ANY&lt;/strong&gt; JS framework or place that returns HTML and you can progressively enchance applications. 🤩&lt;/p&gt;
&lt;p&gt;Since they are custom HTML tags, if you swap implementations, you do not need to update where it is used and you can ship components a separate files &lt;a href=&quot;https://world.hey.com/dhh/modern-web-apps-without-javascript-bundling-or-transpiling-a20f2755&quot;&gt;instead of one big bundle&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Dart &lt;a href=&quot;https://github.com/dart-archive/web-components&quot;&gt;used to support Web Components&lt;/a&gt; at one point and was even used by a precursor to Lit in a product call &lt;a href=&quot;https://github.com/polymer-dart&quot;&gt;Polymer&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Creating a Web Component in Javascript&lt;/h2&gt;
&lt;p&gt;To create a web component in Javascript you just need to extend HTML element and provide callbacks for when the component is mounted.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class HelloWorld extends HTMLElement {
  static observedAttributes = [&amp;quot;name&amp;quot;];

  constructor() {
    super();
  }

  update() {
    this.innerHTML = `Hello: ${this.getAttribute(&apos;name&apos;)}`;
  }

  connectedCallback() {
    console.log(&amp;quot;Custom element added to page.&amp;quot;);
    this. update();
  }

  disconnectedCallback() {
    console.log(&amp;quot;Custom element removed from page.&amp;quot;);
  }

  adoptedCallback() {
    console.log(&amp;quot;Custom element moved to new page.&amp;quot;);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`Attribute ${name} has changed.`);
    if (name === &apos;name&apos;) {
      this. update();
    }
  }
}

customElements.define(&amp;quot;hello-world&amp;quot;, HelloWorld);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can then use it in HTML like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;html&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;hello-world name=&amp;quot;Rody&amp;quot;&amp;gt;&amp;lt;/hello-world&amp;gt;
    &amp;lt;script src=&amp;quot;./index.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works really well, and we don&apos;t even need a build step to create them!&lt;/p&gt;
&lt;h2&gt;Creating Web Components with Dart&lt;/h2&gt;
&lt;p&gt;To create them on the Dart side we need to use the &lt;a href=&quot;https://dart.dev/interop/js-interop/usage&quot;&gt;js_interop package&lt;/a&gt; and the new &lt;a href=&quot;https://pub.dev/packages/web&quot;&gt;web package&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We need to create a factory on the dart side that can create these JS classes without actually being able to create a class in the normal way (since JS and Dart classes are different).&lt;/p&gt;
&lt;p&gt;There is a great API &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct&quot;&gt;&lt;code&gt;Reflect.construct()&lt;/code&gt;&lt;/a&gt; which allows us to take a normal function and invoke it class a class constructor. JavaScript did not always support native classes and was only added with &lt;a href=&quot;https://www.w3schools.com/js/js_es6.asp&quot;&gt;ES6&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By using this built in API, we can create the classes with just pure Dart:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:js_interop&apos;;
import &apos;dart:js_interop_unsafe&apos;;

import &apos;package:web/web.dart&apos;;

class WebComponent&amp;lt;T extends HTMLElement&amp;gt; {
  late T element;
  final String extendsType = &apos;HTMLElement&apos;;

  void connectedCallback() {}

  void disconnectedCallback() {}

  void adoptedCallback() {}

  void attributeChangedCallback(
    String name,
    String? oldValue,
    String? newValue,
  ) {}

  Iterable&amp;lt;String&amp;gt; get observedAttributes =&amp;gt; [];

  bool get formAssociated =&amp;gt; false;

  ElementInternals? get internals =&amp;gt; element[&apos;_internals&apos;] as ElementInternals?;
  set internals(ElementInternals? value) {
    element[&apos;_internals&apos;] = value;
  }

  R getRoot&amp;lt;R extends JSObject&amp;gt;() {
    final hasShadow = element.shadowRoot != null;
    return (hasShadow ? element.shadowRoot! : element) as R;
  }

  static void define(String tag, WebComponent Function() create) {
    final obj = _factory(create);
    window.customElements.define(tag, obj);
  }
}

@JS(&apos;Reflect.construct&apos;)
external JSAny _reflectConstruct(
  JSObject target,
  JSAny args,
  JSFunction constructor,
);

final _instances = &amp;lt;HTMLElement, WebComponent&amp;gt;{};

JSFunction _factory(WebComponent Function() create) {
  final base = create();
  final elemProto = globalContext[base.extendsType] as JSObject;
  late JSAny obj;

  JSAny constructor() {
    final args = &amp;lt;String&amp;gt;[].jsify()!;
    final self = _reflectConstruct(elemProto, args, obj as JSFunction);
    final el = self as HTMLElement;
    _instances.putIfAbsent(el, () =&amp;gt; create()..element = el);
    return self;
  }

  obj = constructor.toJS;
  obj = obj as JSObject;

  final observedAttributes = base.observedAttributes;
  final formAssociated = base.formAssociated;

  obj[&apos;prototype&apos;] = elemProto[&apos;prototype&apos;];
  obj[&apos;observedAttributes&apos;] = observedAttributes.toList().jsify()!;
  obj[&apos;formAssociated&apos;] = formAssociated.jsify()!;

  final prototype = obj[&apos;prototype&apos;] as JSObject;
  prototype[&apos;connectedCallback&apos;] = (HTMLElement instance) {
    _instances[instance]?.connectedCallback();
  }.toJSCaptureThis;
  prototype[&apos;disconnectedCallback&apos;] = (HTMLElement instance) {
    _instances[instance]?.disconnectedCallback();
    _instances.remove(instance);
  }.toJSCaptureThis;
  prototype[&apos;adoptedCallback&apos;] = (HTMLElement instance) {
    _instances[instance]?.adoptedCallback();
  }.toJSCaptureThis;
  prototype[&apos;attributeChangedCallback&apos;] = (
    HTMLElement instance,
    String name,
    String? oldName,
    String? newName,
  ) {
    _instances[instance]?.attributeChangedCallback(name, oldName, newName);
  }.toJSCaptureThis;

  return obj as JSFunction;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This may seem like a lot to digest, but that is ok. It simply does some JS magic to upgrade functions to classes and provide the correct callbacks to create the web components.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you want a package that does this for you, &lt;a href=&quot;https://pub.dev/packages/html_web_components&quot;&gt;html_web_components&lt;/a&gt; is on pub.dev.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To create a Web Component like we did before, we can just extend the class and define the component.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:html_web_components/html_web_components.dart&apos;;

class HelloWorld extends WebComponent {
  @override
  List&amp;lt;String&amp;gt; observedAttributes = [&apos;name&apos;];

  void update() {
    element.innerText = &amp;quot;Hello: ${element.getAttribute(&apos;name&apos;)}!&amp;quot;;
  }

  @override
  void connectedCallback() {
    super.connectedCallback();
    update();
  }

  @override
  void attributeChangedCallback(
    String name,
    String? oldValue,
    String? newValue,
  ) {
    super.attributeChangedCallback(name, oldValue, newValue);
    if (observedAttributes.contains(name)) {
      update();
    }
  }
}

void main() {
  WebComponent.define(&apos;hello-world&apos;, HelloWorld.new);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should look very similar (that is the goal) and makes it so easy to publish the compoents or build a full web application with it.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Web Components allow you to upgrade your client side interactivity while having the freedom to use server rendering to create the template files or just use a SPA on the frontend. You can take these components and use them in &lt;strong&gt;ANY&lt;/strong&gt; JS frameworks! 🤯&lt;/p&gt;
&lt;p&gt;I would highly suggest that you try it out for yourself before you write off Web Components. This is especially true for Flutter developers wanting an alternative to Flutter web (and even use with &lt;a href=&quot;https://pub.dev/packages/jaspr&quot;&gt;Jaspr&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;You can take advantage of Dart&apos;s great ecosystem of packages on &lt;a href=&quot;https://pub.dev/&quot;&gt;pub.dev&lt;/a&gt; and the ability to compile to WASM and JS. If you use a builder like &lt;a href=&quot;https://pub.dev/packages/peanut&quot;&gt;peanut&lt;/a&gt; it will even create the script that tries to load WASM and can fallback to JS for you 🔥&lt;/p&gt;
&lt;p&gt;If you want to see the code, you can &lt;a href=&quot;https://github.com/rodydavis/dart-web-components&quot;&gt;find it on GitHub&lt;/a&gt;. Reach out if you have any questions or want to show off something cool you built with them!&lt;/p&gt;
</content:encoded></item><item><title>Calling the PaLM 2 API with Dart and Flutter</title><link>https://rodydavis.com/dart/palm-2-api/</link><guid isPermaLink="true">https://rodydavis.com/dart/palm-2-api/</guid><description>This tutorial guides Flutter developers through calling the PaLM 2 API using Dart, including setup, API key management, and an example API wrapper.</description><pubDate>Sun, 19 Jan 2025 07:17:50 GMT</pubDate><content:encoded>&lt;h1&gt;Calling the PaLM 2 API with Dart and Flutter&lt;/h1&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;h3&gt;Prompt Template&lt;/h3&gt;
&lt;p&gt;To call the API with a prompt we can start by creating a template:&lt;/p&gt;
&lt;p&gt;This post will talk about how to call the PaLM 2 API with pure Dart in Flutter.&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Make sure you have the &lt;a href=&quot;https://docs.flutter.dev/get-started/install&quot;&gt;Flutter SDK installed&lt;/a&gt; and an &lt;a href=&quot;https://developers.generativeai.google/tutorials/setup&quot;&gt;API key for the PaLM API&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In a terminal window create a new directory and Flutter project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir flutter_ai_example
cd flutter_ai_example
flutter create .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run the following to add the required dependencies to you &lt;code&gt;pubspec.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter pub add flutter_dotenv mustache_template http
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;API Keys&lt;/h3&gt;
&lt;p&gt;Now create a &lt;code&gt;.env&lt;/code&gt; and the root of the project and add the &lt;a href=&quot;https://developers.generativeai.google/tutorials/setup&quot;&gt;API key&lt;/a&gt; you created earlier to the file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;GOOGLE_AI_API_KEY=[Your API Key Here]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/palm_1_q1r92hn4gb.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Update &lt;code&gt;lib/main.dart&lt;/code&gt; main method to be async and load the &lt;code&gt;.env&lt;/code&gt; file on launch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;+ import &apos;package:flutter_dotenv/flutter_dotenv.dart&apos;;

- void main() {
+ Future&amp;lt;void&amp;gt; main() async {
+	await dotenv.load(fileName: &amp;quot;.env&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;This example loads the API key into the bundle as plain text but for production apps you need to call the API from a server&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Firebase has an &lt;a href=&quot;https://extensions.dev/extensions/googlecloud/palm-secure-backend&quot;&gt;extension for securely calling&lt;/a&gt; the API from your mobile app. 🎉&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/palm_2_twuynn8nx3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;In addition you can help prevent abuse to your API by using &lt;a href=&quot;https://firebase.google.com/docs/app-check&quot;&gt;App Check&lt;/a&gt;. 🔐&lt;/p&gt;
&lt;p&gt;If you want to learn more check out &lt;a href=&quot;https://firebase.google.com/codelabs/appcheck-web#0&quot;&gt;this codelab&lt;/a&gt;. 👀&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/palm_3_mbtl09n8wz.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;API Wrapper&lt;/h3&gt;
&lt;p&gt;Create a new file called &lt;code&gt;lib/palm_api.dart&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:convert&apos;;

import &apos;package:flutter_dotenv/flutter_dotenv.dart&apos;;
import &apos;package:mustache_template/mustache_template.dart&apos;;
import &apos;package:http/http.dart&apos; as http;

class PalmApi {
  final String template;
  final String model;

  PalmApi(
    this.template, {
    this.model = &apos;text-bison-001&apos;,
    this.settings = const {
      &amp;quot;temperature&amp;quot;: 0.7,
      &amp;quot;top_k&amp;quot;: 40,
      &amp;quot;top_p&amp;quot;: 0.95,
      &amp;quot;candidate_count&amp;quot;: 1,
      &amp;quot;max_output_tokens&amp;quot;: 1024,
      &amp;quot;stop_sequences&amp;quot;: [],
      &amp;quot;safety_settings&amp;quot;: [
        {&amp;quot;category&amp;quot;: &amp;quot;HARM_CATEGORY_DEROGATORY&amp;quot;, &amp;quot;threshold&amp;quot;: 1},
        {&amp;quot;category&amp;quot;: &amp;quot;HARM_CATEGORY_TOXICITY&amp;quot;, &amp;quot;threshold&amp;quot;: 1},
        {&amp;quot;category&amp;quot;: &amp;quot;HARM_CATEGORY_VIOLENCE&amp;quot;, &amp;quot;threshold&amp;quot;: 2},
        {&amp;quot;category&amp;quot;: &amp;quot;HARM_CATEGORY_SEXUAL&amp;quot;, &amp;quot;threshold&amp;quot;: 2},
        {&amp;quot;category&amp;quot;: &amp;quot;HARM_CATEGORY_MEDICAL&amp;quot;, &amp;quot;threshold&amp;quot;: 2},
        {&amp;quot;category&amp;quot;: &amp;quot;HARM_CATEGORY_DANGEROUS&amp;quot;, &amp;quot;threshold&amp;quot;: 2}
      ]
    },
  });

  Uri _createUrl(String apiKey) {
    const domain = &apos;https://generativelanguage.googleapis.com&apos;;
    final path = &apos;v1beta3/models/$model:generateText&apos;;
    return Uri.parse(&apos;$domain/$path?key=$apiKey&apos;);
  }

  final Map&amp;lt;String, Object?&amp;gt; settings;

  Future&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; execute([Map&amp;lt;String, Object?&amp;gt; args = const {}]) async {
    // Load the API key from .env
    final apiKey = dotenv.env[&apos;GOOGLE_AI_API_KEY&apos;];
    if (apiKey == null) throw Exception(&apos;GOOGLE_AI_API_KEY Required in .env&apos;);

    // Create the API url to call the correct model
    final uri = _createUrl(apiKey);

    // Render the prompt with the tokens
    final text = Template(template).renderString(args);

    // Call the API with the model settings and prompt template
    final response = await http.post(
      uri,
      headers: {&apos;Content-Type&apos;: &apos;application/json&apos;},
      body: jsonEncode({
        &amp;quot;prompt&amp;quot;: {&amp;quot;text&amp;quot;: text},
        ...settings
      }),
    );

    // Check for a successful response and parse out the results
    if (response.statusCode == 200) {
      final body = response.body;
      final json = jsonDecode(body) as Map&amp;lt;String, dynamic&amp;gt;;
      final candidates = (json[&apos;candidates&apos;] as List&amp;lt;dynamic&amp;gt;)
          .map((e) =&amp;gt; e as Map&amp;lt;String, dynamic&amp;gt;)
          .toList();
      return candidates.map((e) =&amp;gt; e[&apos;output&apos;] as String).toList();
    }

    // Default to empty result set
    return [];
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are using &lt;a href=&quot;https://mustache.github.io/&quot;&gt;Mustache&lt;/a&gt; to add tokens to our prompt that we pass to the API. This will feel familiar if you are used to working with &lt;a href=&quot;https://developers.generativeai.google/products/makersuite&quot;&gt;MakerSuite&lt;/a&gt; and makes it easier to iterate on prompts and desired outputs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also copy the config from MakerSuite if you want to change the model or other settings.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;h3&gt;Prompt Template&lt;/h3&gt;
&lt;p&gt;To call the API with a prompt we can start by creating a template:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;const _template = &apos;&apos;&apos;
Generate a valid SVG that does not include animations based on the following UI description &amp;quot;{{description}}&amp;quot;.

Make sure that the content is centered in the middle of the svg.

The width will be {{width}}.
The height will be {{height}}.
The viewbox will be 0 0 {{width}} {{height}}.
&apos;&apos;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are adding tokens for description, width, and height that we will pass in later. Notice that the template is a &lt;code&gt;const&lt;/code&gt; since it will reuse this string at compile time.&lt;/p&gt;
&lt;p&gt;Now we can create the API wrapper:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final api = PalmApi(_template);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Passing Tokens&lt;/h3&gt;
&lt;p&gt;To call the API we can pass in the tokens we defined earlier:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final results = await api.execute({
  &apos;description&apos;: &apos;Red rectangle with blue circle&apos;,
  &apos;width&apos;: 300,
  &apos;height&apos;: 600,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the API can return different results we can check for just the SVG code and extract it out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final res = results.first;
var result = res.trim();
final startIdx = result.indexOf(&apos;```&apos;);
final endIdx = result.lastIndexOf(&apos;```&apos;);
result = result.substring(startIdx + 3, endIdx).trim();
if (result.startsWith(&apos;svg&apos;)) {
  result = result.replaceFirst(&apos;svg&apos;, &apos;&apos;).trim();
}
if (result.startsWith(&apos;xml&apos;)) {
  result = result.replaceFirst(&apos;xml&apos;, &apos;&apos;).trim();
}
result = result.trim();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;result&lt;/code&gt; is not empty then we can work with it as an SVG string now 🎉&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter_svg/flutter_svg.dart&apos;;
...
SvgPicture(SvgStringLoader(result))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you still want to explore PaLM 2 in Flutter further check out this &lt;a href=&quot;https://codelabs.developers.google.com/haiku-generator#0&quot;&gt;codelab&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>How to Print Multiple Objects to the Console with print() in Dart</title><link>https://rodydavis.com/dart/print-multiple-objects/</link><guid isPermaLink="true">https://rodydavis.com/dart/print-multiple-objects/</guid><description>Print multiple objects in Dart using Records for a concise output, similar to JavaScript&apos;s `console.log`.</description><pubDate>Sun, 19 Jan 2025 02:39:50 GMT</pubDate><content:encoded>&lt;h1&gt;How to Print Multiple Objects to the Console with print() in Dart&lt;/h1&gt;
&lt;p&gt;If you are coming from JavaScript you may be used to printing multiple objects to the console with &lt;code&gt;console.log()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;console.log(&apos;a&apos;, 1, &apos;b&apos;, 2); // a 1 b 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Dart we can only print &lt;code&gt;Object?&lt;/code&gt; to the console with &lt;a href=&quot;https://api.dart.dev/stable/3.3.1/dart-core/print.html&quot;&gt;&lt;code&gt;print()&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;print(1); // 1
print(null); // null
print({&apos;a&apos;: 1, &apos;b&apos;: 2}); // {a: 1, b: 1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But it is totally possible to print multiple objects too, we need to use &lt;a href=&quot;https://dart.dev/language/records&quot;&gt;Records&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final number = 1;
final str = &apos;Hello World&apos;;

print((number, str));

print((DateTime.now(), str));

print((DateTime.now(), count: number, description: str));

print((DateTime.now(), StackTrace.current));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Print the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;(1, Hello World)
(2024-03-06 15:48:26.514, Hello World)
(2024-03-06 15:48:26.514, count: 1, description: Hello World)
(2024-03-06 15:48:26.514, Error
    at get current [as current] (https://storage.googleapis.com/nnbd_artifacts/3.3.0/dart_sdk.js:139991:30)
    at Object.main$0 [as main] (&amp;lt;anonymous&amp;gt;:52:94)
    at Object.main$ [as main] (&amp;lt;anonymous&amp;gt;:44:10)
    at &amp;lt;anonymous&amp;gt;:89:26
    at Object.execCb (https://dartpad.dev/require.js:5:16727)
    at e.check (https://dartpad.dev/require.js:5:10499)
    at e.&amp;lt;anonymous&amp;gt; (https://dartpad.dev/require.js:5:12915)
    at https://dartpad.dev/require.js:5:1542
    at https://dartpad.dev/require.js:5:13376
    at each (https://dartpad.dev/require.js:5:1020)
    at e.emit (https://dartpad.dev/require.js:5:13344)
    at e.check (https://dartpad.dev/require.js:5:11058)
    at e.enable (https://dartpad.dev/require.js:5:13242)
    at e.init (https://dartpad.dev/require.js:5:9605)
    at a (https://dartpad.dev/require.js:5:8305)
    at Object.completeLoad (https://dartpad.dev/require.js:5:15962)
    at HTMLScriptElement.onScriptLoad (https://dartpad.dev/require.js:5:16882))

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
</content:encoded></item><item><title>Check if an Object is Truthy in Dart</title><link>https://rodydavis.com/dart/truthy/</link><guid isPermaLink="true">https://rodydavis.com/dart/truthy/</guid><description>Dart doesn&apos;t natively support truthy checks like JavaScript, but you can add a custom extension to treat objects as truthy, handling null, boolean, numeric, string, iterable, and map types.</description><pubDate>Sun, 19 Jan 2025 02:25:47 GMT</pubDate><content:encoded>&lt;h1&gt;Check if an Object is Truthy in Dart&lt;/h1&gt;
&lt;p&gt;If you are coming from language like JavaScript you may be used to checking if an object is &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Truthy&quot;&gt;truthy&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (true)
if ({})
if ([])
if (42)
if (&amp;quot;0&amp;quot;)
if (&amp;quot;false&amp;quot;)
if (new Date())
if (-42)
if (12n)
if (3.14)
if (-3.14)
if (Infinity)
if (-Infinity)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Dart you need to explicitly check if an object is not null, true/false or determine if the value is true based on the type.&lt;/p&gt;
&lt;p&gt;It is possible however to use Dart extensions to add the truthy capability.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;extension on Object? {
  bool get isTruthy =&amp;gt; truthy(this);
}

bool truthy(Object? val) {
  if (val == null) return false;
  if (val is bool) return val;
  if (val is num &amp;amp;&amp;amp; val == 0) return false;
  if (val is String &amp;amp;&amp;amp; (val == &apos;false&apos; || val == &apos;&apos;)) return false;
  if (val is Iterable &amp;amp;&amp;amp; val.isEmpty) return false;
  if (val is Map &amp;amp;&amp;amp; val.isEmpty) return false;
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will now make it possible for any object to be evaluated as a truthy value in if statements or value assignments.&lt;/p&gt;
&lt;p&gt;Prints the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;(null, false)
(, false)
(false, false)
(true, true)
(0, false)
(1, true)
(false, false)
(true, true)
([], false)
([1, 2, 3], true)
({}, false)
({1, 2, 3}, true)
({a: 1, b: 2}, true)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
</content:encoded></item><item><title>Automate Flutter App Releases</title><link>https://rodydavis.com/flutter/automation/</link><guid isPermaLink="true">https://rodydavis.com/flutter/automation/</guid><description>Automates Flutter app release processes, including building, committing changes, and deploying to beta and production via Fastlane.</description><pubDate>Sun, 19 Jan 2025 06:09:34 GMT</pubDate><content:encoded>&lt;h1&gt;Automate Flutter App Releases&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; You can find the script &lt;a href=&quot;https://gist.github.com/rodydavis/774b36e32d7efa882cca8dd16da6e74c&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;#!/bin/bash

echo &amp;quot;App Release Automator by @rodydavis&amp;quot;

action=&amp;quot;$1&amp;quot;
red=`tput setaf 1`
green=`tput setaf 2`
reset=`tput sgr0`

if [ ${action} = &amp;quot;build&amp;quot; ]; then

    echo &amp;quot;${green}Generating built files.. ${reset}&amp;quot;
    flutter packages pub run build_runner clean
    flutter packages pub run build_runner build --delete-conflicting-outputs

    pub global activate pubspec_version
    git commit -a -m &amp;quot;Build $(pubver bump patch)&amp;quot;
    
    echo &amp;quot;${green}Building Project...${reset}&amp;quot;
    find . -name &amp;quot;*-e&amp;quot; -type f -delete
    flutter format .
    flutter clean

    echo &amp;quot;${green}Project Size: $(find . -name &amp;quot;*.dart&amp;quot; | xargs cat | wc -c)${reset}&amp;quot;

    echo &amp;quot;${green}Building APK...${reset}&amp;quot;
    flutter build apk

    echo &amp;quot;${green}Builing IPA..${reset}&amp;quot;
    cd ./ios &amp;amp;&amp;amp; pod install &amp;amp;&amp;amp; pod repo update &amp;amp;&amp;amp; cd ..
    flutter build ios

    git commit -a -m &amp;quot;Project Rebuilt&amp;quot;


elif [ ${action} = &amp;quot;beta&amp;quot; ]; then

    echo &amp;quot;${green}Generating built files..${reset}&amp;quot;
    flutter packages pub run build_runner clean
    flutter packages pub run build_runner build --delete-conflicting-outputs

    pub global activate pubspec_version
    git commit -a -m &amp;quot;Beta $(pubver bump patch)&amp;quot;
    
    echo &amp;quot;${green}Building Project...${reset}&amp;quot;
    find . -name &amp;quot;*-e&amp;quot; -type f -delete
    flutter format .
    flutter clean

    echo &amp;quot;${green}Project Size: $(find . -name &amp;quot;*.dart&amp;quot; | xargs cat | wc -c)${reset}&amp;quot;

    echo &amp;quot;${green}Building APK...${reset}&amp;quot;
    flutter build apk

    echo &amp;quot;${green}Sending Android to Beta...${reset}&amp;quot;
    cd ./android &amp;amp;&amp;amp; fastlane beta &amp;amp;&amp;amp; cd ..

    echo &amp;quot;${green}Builing IPA..${reset}&amp;quot;
    flutter build ios

    echo &amp;quot;${green}Sending iOS to Beta..${reset}&amp;quot;
    cd ./ios &amp;amp;&amp;amp; fastlane beta &amp;amp;&amp;amp; cd ..

    git commit -a -m &amp;quot;Sent to Beta&amp;quot;


elif [ ${action} = &amp;quot;release&amp;quot; ]; then

    echo &amp;quot;${green}Generating built files..${reset}&amp;quot;
    flutter packages pub run build_runner clean
    flutter packages pub run build_runner build --delete-conflicting-outputs

    pub global activate pubspec_version
    git commit -a -m &amp;quot;Production $(pubver bump minor)&amp;quot;

    echo &amp;quot;${green}Building Project...${reset}&amp;quot;
    find . -name &amp;quot;*-e&amp;quot; -type f -delete
    flutter format .
    flutter clean

    echo &amp;quot;${green}Project Size: $(find . -name &amp;quot;*.dart&amp;quot; | xargs cat | wc -c)${reset}&amp;quot;

    echo &amp;quot;${green}Building APK...${reset}&amp;quot;
    flutter build apk

    echo &amp;quot;${green}Sending Android to Production...${reset}&amp;quot;
    cd ./android &amp;amp;&amp;amp; fastlane release &amp;amp;&amp;amp; cd ..

    echo &amp;quot;${green}Builing IPA..${reset}&amp;quot;
    flutter build ios

    echo &amp;quot;${green}Sending iOS to Production...${reset}&amp;quot;
    cd ./ios &amp;amp;&amp;amp; fastlane release &amp;amp;&amp;amp; cd ..

    git commit -a -m &amp;quot;Sent to Production&amp;quot;

fi

echo &amp;quot;${green}Successfully completed${reset}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fastlane setup in each directory&lt;/li&gt;
&lt;li&gt;build_runner as a dependency&lt;/li&gt;
&lt;li&gt;Git Project in VCS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Steps to Run:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download this file and put it at the root level of your flutter project&lt;/li&gt;
&lt;li&gt;Open the terminal and navigate to your project location&lt;/li&gt;
&lt;li&gt;Enter this command: &lt;code&gt;chmod +x release.sh&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Usage&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For beta: &lt;code&gt;./release.sh beta&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For production: &lt;code&gt;./release.sh release&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It will do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bump the version numbers if you are using the version in the &lt;code&gt;pubspec.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Release the apps with Fastlane&lt;/li&gt;
&lt;li&gt;Format all Dart Files&lt;/li&gt;
&lt;li&gt;Clean Project&lt;/li&gt;
&lt;li&gt;Rebuild classes&lt;/li&gt;
&lt;li&gt;Add commit message&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make your life easier and automate your builds to beta and production!&lt;/p&gt;
&lt;h2&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://fastlane.tools/&quot;&gt;Fastlane&lt;/a&gt; setup in each directory&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pub.dartlang.org/packages/build_runner&quot;&gt;build_runner&lt;/a&gt; as a dependency&lt;/li&gt;
&lt;li&gt;Git Project in VCS&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Initial Setup&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Download &lt;a href=&quot;https://gist.github.com/rodydavis/774b36e32d7efa882cca8dd16da6e74c&quot;&gt;this file&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Put it at the root level of your flutter project&lt;/li&gt;
&lt;li&gt;Open the terminal and navigate to your project location&lt;/li&gt;
&lt;li&gt;Enter this command: chmod +x release.sh&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;Now you can call this script!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For beta: &lt;code&gt;./release.sh beta&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For production: &lt;code&gt;./release.sh release&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Bump the version numbers if you are using the version in the pubspec.yaml&lt;/li&gt;
&lt;li&gt;Release the apps with Fastlane&lt;/li&gt;
&lt;li&gt;Format all Dart Files&lt;/li&gt;
&lt;li&gt;Clean Project&lt;/li&gt;
&lt;li&gt;Rebuild classes&lt;/li&gt;
&lt;li&gt;Add commit messages&lt;/li&gt;
&lt;li&gt;Updates Cocoa Pods&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Flutter Terminal Cheat Sheet</title><link>https://rodydavis.com/flutter/cheat-sheet/</link><guid isPermaLink="true">https://rodydavis.com/flutter/cheat-sheet/</guid><description>A collection of Flutter commands for web development, including configuration, build processes, debugging, testing, and project cleanup.</description><pubDate>Mon, 20 Jan 2025 00:13:39 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Terminal Cheat Sheet&lt;/h1&gt;
&lt;h2&gt;Run Flutter web with SKIA&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter run -d web --release --dart-define=FLUTTER_WEB_USE_SKIA=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Run Flutter web with Canvas Kit&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter run -d chrome --release --dart-define=FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Build your Flutter web app to Github Pages to the docs folder&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter build web &amp;amp;&amp;amp; rm -rf ./docs &amp;amp;&amp;amp; mkdir ./docs &amp;amp;&amp;amp; cp -a ./build/web/. ./docs/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Clean rebuild CocoaPods&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd ios &amp;amp;&amp;amp; pod deintegrate &amp;amp;&amp;amp; pod cache clean —all &amp;amp;&amp;amp; pod install &amp;amp;&amp;amp; cd ..
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Sometimes with firebase you need to run: &lt;code&gt;pod update Firebase&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Create Dart package with Example&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter create -t plugin . &amp;amp;&amp;amp; flutter create -i swift -a kotlin --androidx example
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Watch Build Files&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter packages pub run build_runner watch  -—delete-conflicting-outputs
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Generate Build Files&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter packages pub run build_runner build  -—delete-conflicting-outputs
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Build Bug Report&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter run —bug-report
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flutter generate test coverage&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter test --coverage &amp;amp;&amp;amp; genhtml -o coverage coverage/lcov.info
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rebuild Flutter Cache&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter pub pub cache repair
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Clean every flutter project&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;find . -name &amp;quot;pubspec.yaml&amp;quot; -exec $SHELL -c &apos;
    echo &amp;quot;Done. Cleaning all projects.&amp;quot;
    for i in &amp;quot;$@&amp;quot; ; do
        DIR=$(dirname &amp;quot;${i}&amp;quot;)
        echo &amp;quot;Cleaning ${DIR}...&amp;quot;
        (cd &amp;quot;$DIR&amp;quot; &amp;amp;&amp;amp; flutter clean &amp;gt;/dev/null 2&amp;gt;&amp;amp;1)
    done
    echo &amp;quot;DONE!&amp;quot;
&apos; {} +
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conditional Export/Import&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;export &apos;unsupported.dart&apos;
    if (dart.library.html) &apos;web.dart&apos;
    if (dart.library.io) &apos;mobile.dart&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kill Dart Running&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;killall -9 dart
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flutter scripts&lt;/h2&gt;
&lt;p&gt;Add all the scripts to your &lt;code&gt;pubspec.yaml&lt;/code&gt; with &lt;a href=&quot;https://pub.dev/packages/flutter_scripts&quot;&gt;flutter_scripts&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Displaying HTML in Flutter</title><link>https://rodydavis.com/flutter/display-html/</link><guid isPermaLink="true">https://rodydavis.com/flutter/display-html/</guid><description>Display HTML content in Flutter using the `easy_web_view` package for both web and mobile platforms, providing a convenient way to render HTML and Markdown content within your Flutter applications.</description><pubDate>Sun, 19 Jan 2025 08:08:22 GMT</pubDate><content:encoded>&lt;h1&gt;Displaying HTML in Flutter&lt;/h1&gt;
&lt;p&gt;Sometimes you have content in HTML that needs to be displayed and interacted with in Flutter.&lt;/p&gt;
&lt;p&gt;Online Demo:&lt;a href=&quot;https://rodydavis.github.io/easy_web_view/#/&quot;&gt;https://rodydavis.github.io/easy_web_view/#/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/html_1_yaak9yba8y.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;For those impatient I created a package for you to get all the following functionally and more here: &lt;a href=&quot;https://pub.dev/packages/easy_web_view&quot;&gt;https://pub.dev/packages/easy_web_view&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;Create a new flutter project named whatever you want.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you plan on showing HTML content on iOS/Android you will need to add the following to your pubspec.yaml&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;dependencies:
    webview_flutter: ^0.3.15+1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Web&lt;/h2&gt;
&lt;p&gt;Reference: &lt;a href=&quot;https://github.com/rodydavis/easy_web_view/blob/master/lib/src/web.dart&quot;&gt;/lib/src/web.dart&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To show html on Flutter web we need to use an HTMLElementView. This is a platform view that allows us to display native content.&lt;/p&gt;
&lt;p&gt;We first need to register the Element and add all the options we need. Here we are creating an iFrame element and setting the source based on if it is markdown, html or a url.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/html_2_uowumu74gx.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;To display valid HTML you can set the src field to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;_src = &amp;quot;data:text/html;charset=utf-8,&amp;quot; + Uri.encodeComponent(&amp;quot;HTML_CONTENT_HERE&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;For the package you can also pass markdown to the src and it will convert it for you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After you call the setup method it is now time to display your new platform view:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/html_3_d2x4h481p0.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You need to use the same viewType string as you registered for “registerViewFactory” method earlier.&lt;/p&gt;
&lt;p&gt;Finally you need to wrap it in a container or sized box with an explicit width and height!&lt;/p&gt;
&lt;h2&gt;Mobile&lt;/h2&gt;
&lt;p&gt;Reference: &lt;a href=&quot;https://github.com/rodydavis/easy_web_view/blob/master/lib/src/mobile.dart&quot;&gt;https://github.com/rodydavis/easy_web_view/blob/master/lib/src/mobile.dart&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Mobile setup should be easier. Let’s add a method for updating the url that we will pass to the web view.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/html_4_can4qsn59m.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Create the controller:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;WebViewController _controller;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And when ever the src changes call this method:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;_controller.loadUrl(_updateUrl(widget.src), headers: widget.headers);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally lets show the html in the widget tree:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/html_5_ue0rqxe1sr.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to see a complete example and advanced use case view the source here: &lt;a href=&quot;https://github.com/rodydavis/easy_web_view&quot;&gt;https://github.com/rodydavis/easy_web_view&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And if you just want to have it all done for you use this package: &lt;a href=&quot;https://pub.dev/packages/easy_web_view&quot;&gt;https://pub.dev/packages/easy_web_view&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Feel free to make PRs if you have anything that could help make it better too (Or if you find bugs).&lt;/p&gt;
&lt;p&gt;When you show HTML this way you will find that you can interact, select text and work with it just like you would it it were a regular web page. If you are using the package you can also just pass embedded content or html elements too without needing a full html valid file (YouTube video for example).&lt;/p&gt;
</content:encoded></item><item><title>Creating Your First Flutter Project</title><link>https://rodydavis.com/flutter/getting-started/</link><guid isPermaLink="true">https://rodydavis.com/flutter/getting-started/</guid><description>Learn how to install Flutter and create your first Flutter project for cross-platform app development on web, iOS, Android, and more.</description><pubDate>Sun, 19 Jan 2025 07:34:27 GMT</pubDate><content:encoded>&lt;h1&gt;Creating Your First Flutter Project&lt;/h1&gt;
&lt;p&gt;Flutter is a UI Toolkit from Google allowing you to create expressive and unique experiences unmatched on any platform. You can write your UI once and run it everywhere. Yes everywhere! Web, iOS, Android, Windows, Linux, MacOS, Raspberry PI and much more…&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/flutter_1_zxbnyzfn8p.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you prefer a video you can follow the YouTube series I am doing called “Flutter Take 5” where I explore topics that you encounter when building a Flutter application. I will also give you tips and tricks as I go through the series.&lt;/p&gt;
&lt;p&gt;Or this short:&lt;/p&gt;
&lt;h2&gt;What is Flutter&lt;/h2&gt;
&lt;p&gt;Flutter recently crossed React Native on Github and now has more than 2 million developers using Flutter to create applications. There are more than 50,000 apps on Google Play alone published with Flutter.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://flutter.dev/&quot;&gt;Learn about Flutter.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;Getting started is very easy once you get the SDK installed. After it is installed creating new applications, plugins and packages is lighting fast. Follow this guide to install Flutter:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://flutter.dev/docs/get-started/install&quot;&gt;How to install Flutter.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;One nice thing about Flutter is that it is developed in the open as an open source project that anyone can contribute to. If there is something missing you can easily fork the repo and make a PR for the missing functionality.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/flutter_2_7906zvehq3.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Create the Project&lt;/h2&gt;
&lt;p&gt;Now that you have Flutter installed it is time to create your first (Of Many 😉) Flutter project! Open up your terminal and navigate to wherever you want the application folder to be created. Once you “cd” into the directory you can type the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter create my_awesome_project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can replace “my_awesome_project” with whatever you want the project to be called. It is important to use snake_case as it is the valid syntax for project names in dart.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/flutter_3_ex6m9fvb6h.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Congratulations you just created your first project!&lt;/p&gt;
&lt;h2&gt;Open the Project&lt;/h2&gt;
&lt;p&gt;So you may be wondering what we just created so let us dive in to the details. You can open up you project in VSCode if you have it installed by typing the following into terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd my_awesome_project &amp;amp;&amp;amp; code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can open up the folder in your favorite IDE if you prefer. Two important files to notice are the pubspec.yaml and lib/main.dart&lt;/p&gt;
&lt;p&gt;Your UI and Logic is located at “lib/main.dart” and you should see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with &amp;quot;flutter run&amp;quot;. You&apos;ll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // &amp;quot;hot reload&amp;quot; (press &amp;quot;r&amp;quot; in the console where you ran &amp;quot;flutter run&amp;quot;,
        // or simply save your changes to &amp;quot;hot reload&amp;quot; in a Flutter IDE).
        // Notice that the counter didn&apos;t reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
        // This makes the visual density adapt to the platform that you run
        // the app on. For desktop platforms, the controls will be smaller and
        // closer together (more dense) than on mobile platforms.
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: &apos;Flutter Demo Home Page&apos;),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked &amp;quot;final&amp;quot;.

  final String title;

  @override
  _MyHomePageState createState() =&amp;gt; _MyHomePageState();
}

class _MyHomePageState extends State&amp;lt;MyHomePage&amp;gt; {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke &amp;quot;debug painting&amp;quot; (press &amp;quot;p&amp;quot; in the console, choose the
          // &amp;quot;Toggle Debug Paint&amp;quot; action from the Flutter Inspector in Android
          // Studio, or the &amp;quot;Toggle Debug Paint&amp;quot; command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: &amp;lt;Widget&amp;gt;[
            Text(
              &apos;You have pushed the button this many times:&apos;,
            ),
            Text(
              &apos;$_counter&apos;,
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: &apos;Increment&apos;,
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can define any dependencies and plugins needed for the application at “pubspec.yaml” and you should see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;name: example
description: A new Flutter project.

# The following line prevents the package from being accidentally published to
# pub.dev using `pub publish`. This is preferred for private packages.
publish_to: &apos;none&apos; # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1

environment:
  sdk: &amp;quot;&amp;gt;=2.7.0 &amp;lt;3.0.0&amp;quot;

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.3

dev_dependencies:
  flutter_test:
    sdk: flutter

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.webp
  #   - images/a_dot_ham.webp

  # An image asset can refer to one or more resolution-specific &amp;quot;variants&amp;quot;, see
  # https://flutter.dev/assets-and-images/#resolution-aware.

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this &amp;quot;flutter&amp;quot; section. Each entry in this list should have a
  # &amp;quot;family&amp;quot; key with the font family name, and a &amp;quot;fonts&amp;quot; key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Running the Project&lt;/h2&gt;
&lt;p&gt;Running the application is very easy too. While there are buttons in all the IDEs you can also run your project from the command line for quick testing. You can also configure &lt;a href=&quot;https://flutter.dev/desktop&quot;&gt;Flutter for Desktop&lt;/a&gt; and no need to wait for an emulator to warm up. Open your project and enter the following into terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter run -d macos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the “-d macos” as you can customize what device you want to run on. You should see the following in terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Building macOS application...                                           
Syncing files to device macOS...                                   141ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h Repeat this help message.
d Detach (terminate &amp;quot;flutter run&amp;quot; but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
An Observatory debugger and profiler on macOS is available at: [http://127.0.0.1:58932/f1Mspofty_k=/](http://127.0.0.1:58932/f1Mspofty_k=/)
Application finished.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also run multiple devices at the same time. You can find more info on the &lt;a href=&quot;https://github.com/flutter/flutter/wiki/Multi-device-debugging-in-VS-Code&quot;&gt;Flutter Octopus here&lt;/a&gt;. If everything went well you should see the following application launch:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/flutter_4_sh7qzq9ggv.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;It is a pretty basic application at this point but it is important to show how easy it is to change the state in the application. You can rebuild the UI just by calling “setState()”.&lt;/p&gt;
&lt;h2&gt;Testing the Project&lt;/h2&gt;
&lt;p&gt;Testing is one of the reasons I love Flutter so much and it is dead simple to run and write tests for the project. If you look at the file “test/widget_test.dart” you should see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.

import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter_test/flutter_test.dart&apos;;

import &apos;package:example/main.dart&apos;;

void main() {
  testWidgets(&apos;Counter increments smoke test&apos;, (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(MyApp());

    // Verify that our counter starts at 0.
    expect(find.text(&apos;0&apos;), findsOneWidget);
    expect(find.text(&apos;1&apos;), findsNothing);

    // Tap the &apos;+&apos; icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text(&apos;0&apos;), findsNothing);
    expect(find.text(&apos;1&apos;), findsOneWidget);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can run these tests very easily. Open your project and type the following into the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter test
00:07 +1: All tests passed!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just like that all your tests will run and you can catch any bugs you missed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/flutter_5_fr3u6tytdx.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can also generate code coverage for your applications easily by typing the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter test --coverage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will generate a new file at “coverage/lcov.info” and will read the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;SF:lib/main.dart
DA:3,0
DA:4,0
DA:9,1
DA:11,1
DA:13,1
DA:27,1
DA:29,1
DA:35,2
DA:48,1
DA:49,1
DA:55,1
DA:56,2
DA:62,2
DA:66,1
DA:74,1
DA:75,1
DA:78,3
DA:80,1
DA:83,1
DA:99,1
DA:100,1
DA:103,1
DA:104,2
DA:105,3
DA:110,1
DA:111,1
DA:113,1
LF:27
LH:25
end_of_record
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can now easily create badges and graphs with the LCOV data. Here is a package that will make that easier:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pub.dev/packages/test_coverage&quot;&gt;test_coverage | Dart Package&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Flutter makes it possible to build applications very quickly that do not depend on web or mobile technologies. It can familiar to writing a game as you have to design all your own UI. You can find the final source code here:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/rodydavis/flutter_take_5/tree/master/01_your_first_project&quot;&gt;Final source code.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can also find the Flutter source code here:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/flutter/flutter&quot;&gt;Flutter source code.&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>How to Manage Multiple Flutter Versions with Git Worktrees and ZSH</title><link>https://rodydavis.com/flutter/git-worktree-channels/</link><guid isPermaLink="true">https://rodydavis.com/flutter/git-worktree-channels/</guid><description>Manage multiple Flutter SDK versions using Git worktrees and ZSH aliases for easy switching between stable, beta, and master channels.</description><pubDate>Sun, 19 Jan 2025 02:32:18 GMT</pubDate><content:encoded>&lt;h1&gt;How to Manage Multiple Flutter Versions with Git Worktrees and ZSH&lt;/h1&gt;
&lt;p&gt;If you have been using &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt; for any length of time then you probably have needed to use multiple flutter versions across multiple projects.&lt;/p&gt;
&lt;p&gt;In the past I used to use &lt;a href=&quot;https://fvm.app/&quot;&gt;FVM&lt;/a&gt; (Flutter Version Management) which is similar to &lt;a href=&quot;https://github.com/nvm-sh/nvm&quot;&gt;NVM&lt;/a&gt; (Node Version Manager) in the JS world.&lt;/p&gt;
&lt;p&gt;I wanted a solution that only relied on &lt;a href=&quot;https://git-scm.com/&quot;&gt;Git&lt;/a&gt;, and started using &lt;a href=&quot;https://git-scm.com/docs/git-worktree&quot;&gt;worktrees&lt;/a&gt; to manage the Flutter channels.&lt;/p&gt;
&lt;h2&gt;Download the SDK&lt;/h2&gt;
&lt;p&gt;Check out the flutter repo in a known directory, in this case I will download it to &lt;code&gt;~/Developer/&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;git clone https://github.com/flutter/flutter ~/Developer/flutter
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Add Flutter Channels&lt;/h2&gt;
&lt;p&gt;Now we can add the branches we want to track:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd ~/Developer/flutter
git checkout origin/dev
git worktree add ../flutter-stable stable
git worktree add ../flutter-beta beta
git worktree add ../flutter-master master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to checkout the dev channel to allow us to create the worktree for the master branch. This will keep the &lt;code&gt;flutter&lt;/code&gt; directory separate so we can work on PRs and apply local changes.&lt;/p&gt;
&lt;p&gt;After this runs we should have 4 directories: &lt;code&gt;flutter&lt;/code&gt;, &lt;code&gt;flutter-master&lt;/code&gt;, &lt;code&gt;flutter-beta&lt;/code&gt; and &lt;code&gt;flutter-stable&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Add ZSH Alias for each Channel&lt;/h2&gt;
&lt;p&gt;Now we need a way to reference each SDK on the fly with an &lt;a href=&quot;https://github.com/rothgar/mastering-zsh/blob/master/docs/helpers/aliases.md&quot;&gt;alias in ZSH&lt;/a&gt;. Add the following to &lt;code&gt;~/.zshrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;alias flutter-master=&apos;~/Developer/flutter-master/bin/flutter&apos;
alias dart-master=&apos;~/Developer/flutter-master/bin/dart&apos;

alias flutter-beta=&apos;~/Developer/flutter-beta/bin/flutter&apos;
alias dart-beta=&apos;~/Developer/flutter-beta/bin/dart&apos;

alias flutter-stable=&apos;~/Developer/flutter-stable/bin/flutter&apos;
alias dart-stable=&apos;~/Developer/flutter-stable/bin/dart&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;After reopening the terminal, you can verify it is working by running (or add any channel we added above):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter-master doctor
dart-master --version

flutter-stable doctor
dart-stable --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can update any of the channels by navigating to the directory of the worktree for the given channel and pulling changes like any other Git repo.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd ~/Developer/flutter-master
git checkout origin/master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git worktrees are just a way to checkout multiple branches as separate folders instead of needing to stash changes.&lt;/p&gt;
</content:encoded></item><item><title>How to build a graph database with Flutter</title><link>https://rodydavis.com/flutter/graph-database/</link><guid isPermaLink="true">https://rodydavis.com/flutter/graph-database/</guid><description>Build a graph database in Flutter using SQLite and Drift, ideal for modeling relationships between data in applications like social networks, games, and blogs.</description><pubDate>Sat, 18 Jan 2025 21:09:38 GMT</pubDate><content:encoded>&lt;h1&gt;How to build a graph database with Flutter&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to create and use a graph database with &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/flutter_graph_database&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/flutter_graph_database/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Flutter installed and setup (Refer to this &lt;a href=&quot;https://rodydavis.com/posts/first-flutter-project/&quot;&gt;article&lt;/a&gt; if you need help).&lt;/p&gt;
&lt;p&gt;Basic knowledge of &lt;a href=&quot;https://www.sqlite.org/index.html&quot;&gt;SQLite&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Basic knowledge of Graph Databases (Refer to this &lt;a href=&quot;https://www.youtube.com/watch?v=GekQqFZm7mA&quot;&gt;video&lt;/a&gt; if you need to learn more).&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;First of all, why do we need a graph database when other storage options exist?&lt;/p&gt;
&lt;p&gt;Why not use key value stores, document stores, or relational databases?&lt;/p&gt;
&lt;p&gt;Well, the answer is that it depends on the problem you are trying to solve.&lt;/p&gt;
&lt;p&gt;Graph databases are great for modeling relationships between data.&lt;/p&gt;
&lt;p&gt;A couple examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A social network app can model the relationships between users and posts&lt;/li&gt;
&lt;li&gt;A game can model the relationships between players and items&lt;/li&gt;
&lt;li&gt;A blog can model the relationships between posts and comments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The possibilities are endless.&lt;/p&gt;
&lt;p&gt;Instead of storing data in a table for each collection we store the data as a graph in a nodes and edges table with some additional extensions in SQLite to make it easier.&lt;/p&gt;
&lt;p&gt;Here is a &lt;a href=&quot;https://www.hytradboi.com/2022/simple-graph-sqlite-as-probably-the-only-graph-database-youll-ever-need&quot;&gt;page&lt;/a&gt; that goes in to detail about it and showcases what we are trying to build.&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;First we need to create a new Flutter project.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir flutter_graph_database
cd flutter_graph_database
flutter create .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the project is created open it in your favorite code editor.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;code .
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Creating the Database&lt;/h2&gt;
&lt;p&gt;We are going to use the &lt;a href=&quot;hhttps://pub.dev/packages/drift&quot;&gt;drift&lt;/a&gt; package to create the database.&lt;/p&gt;
&lt;p&gt;Update the &lt;strong&gt;pubspec.yaml&lt;/strong&gt; file with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;name: flutter_graph_database
description: A new Flutter package project.
version: 0.0.1
publish_to: none

environment:
  sdk: &amp;quot;&amp;gt;=2.19.0-238.0.dev &amp;lt;3.0.0&amp;quot;
  flutter: &amp;quot;&amp;gt;=1.17.0&amp;quot;

dependencies:
  flutter:
    sdk: flutter
  drift: ^2.1.0
  sqlite3_flutter_libs: ^0.5.5
  http: ^0.13.5
  path_provider: ^2.0.0
  path: ^1.8.2
  sqlite3: ^1.7.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  build_runner: ^2.2.0
  drift_dev: ^2.1.0

flutter:

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Database Connection&lt;/h3&gt;
&lt;p&gt;Next we need to create the database.&lt;/p&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/connection/unsupported.dart&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:drift/drift.dart&apos;;
import &apos;package:drift/native.dart&apos;;

DatabaseConnection connect(
  String dbName, {
  bool useWebWorker = false,
  bool logStatements = false,
}) {
  return DatabaseConnection(NativeDatabase.memory(
    logStatements: logStatements,
  ));
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/connection/native.dart&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:io&apos;;
import &apos;dart:isolate&apos;;

import &apos;package:drift/drift.dart&apos;;
import &apos;package:drift/isolate.dart&apos;;
import &apos;package:drift/native.dart&apos;;
import &apos;package:path_provider/path_provider.dart&apos;;
import &apos;package:path/path.dart&apos; as p;

DatabaseConnection connect(
  String dbName, {
  bool useWebWorker = false,
  bool logStatements = false,
}) {
  return DatabaseConnection.delayed(Future.sync(() async {
    final appDir = await getApplicationDocumentsDirectory();
    final dbPath = p.join(appDir.path, dbName);

    final receiveDriftIsolate = ReceivePort();
    await Isolate.spawn(_entrypointForDriftIsolate,
        _IsolateStartRequest(receiveDriftIsolate.sendPort, dbPath));

    final driftIsolate = await receiveDriftIsolate.first as DriftIsolate;
    return driftIsolate.connect();
  }));
}

class _IsolateStartRequest {
  final SendPort talkToMain;
  final String databasePath;

  _IsolateStartRequest(this.talkToMain, this.databasePath);
}

void _entrypointForDriftIsolate(_IsolateStartRequest request) {
  final databaseImpl = NativeDatabase(
    File(request.databasePath),
    logStatements: false,
  );

  final driftServer = DriftIsolate.inCurrent(
    () =&amp;gt; DatabaseConnection(databaseImpl),
  );

  request.talkToMain.send(driftServer);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/connection/web.dart&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:async&apos;;

// ignore: avoid_web_libraries_in_flutter
import &apos;dart:html&apos;;

import &apos;package:drift/drift.dart&apos;;
import &apos;package:drift/remote.dart&apos;;
import &apos;package:drift/web.dart&apos;;
import &apos;package:drift/wasm.dart&apos;;
import &apos;package:http/http.dart&apos; as http;
import &apos;package:sqlite3/wasm.dart&apos;;

DatabaseConnection connect(
  String dbName, {
  bool useWebWorker = false,
  bool logStatements = false,
}) {
  if (useWebWorker) {
    final worker = SharedWorker(&apos;shared_worker.dart.js&apos;);
    return remote(worker.port!.channel());
  } else {
    return DatabaseConnection.delayed(Future.sync(() async {
      final response = await http.get(Uri.parse(&apos;sqlite3.wasm&apos;));
      final fs = await IndexedDbFileSystem.open(dbName: &apos;/db/&apos;);
      final path = &apos;/drift/db/$dbName&apos;;
      final sqlite3 = await WasmSqlite3.load(
        response.bodyBytes,
        SqliteEnvironment(fileSystem: fs),
      );
      final databaseImpl = WasmDatabase(
        sqlite3: sqlite3,
        path: path,
        fileSystem: fs, // &amp;lt;- this is required but not documented
        logStatements: logStatements,
      );
      return DatabaseConnection(databaseImpl);
    }));
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/connection/connection.dart&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;export &apos;unsupported.dart&apos;
    if (dart.library.js) &apos;web.dart&apos;
    if (dart.library.ffi) &apos;native.dart&apos;;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Database SQL Files&lt;/h3&gt;
&lt;h4&gt;Schema&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/schema.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE IF NOT EXISTS nodes (
    body TEXT,
    id   TEXT GENERATED ALWAYS AS (json_extract(body, &apos;$.id&apos;)) VIRTUAL NOT NULL UNIQUE
);

CREATE INDEX IF NOT EXISTS id_idx ON nodes(id);

CREATE TABLE IF NOT EXISTS edges (
    source     TEXT,
    target     TEXT,
    properties TEXT,
    UNIQUE(source, target, properties) ON CONFLICT REPLACE,
    FOREIGN KEY(source) REFERENCES nodes(id),
    FOREIGN KEY(target) REFERENCES nodes(id)
);

CREATE INDEX IF NOT EXISTS source_idx ON edges(source);
CREATE INDEX IF NOT EXISTS target_idx ON edges(target);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;The ID column is a virtual column that is generated from the body column. This is done so that we can query the database by ID without having to parse the JSON body column.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Queries&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/queries.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

getAllNodes: 
    SELECT * FROM nodes;
getAllEdges: 
    SELECT * FROM edges;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Delete Edge&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/delete-edge.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

deleteEdge:
    DELETE FROM edges 
    WHERE source = ? OR target = ?;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Delete Node&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/delete-node.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

deleteNode: 
    DELETE FROM nodes 
    WHERE id = ?;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Insert Edge&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/insert-edge.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

insertEdge(:source as TEXT, :target as TEXT, :body as TEXT): 
    INSERT INTO edges VALUES(:source, :target, json(:body));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Search Edges Inbound&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/search-edges-inbound.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

searchEdgesInbound: 
    SELECT * FROM edges 
    WHERE source = ?;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Search Edges Outbound&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/search-edges-outbound.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

searchEdgesOutbound: 
    SELECT * FROM edges 
    WHERE target = ?;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Search Edges&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/search-edges.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

searchEdges: 
    SELECT * FROM edges WHERE source = ? 
    UNION 
    SELECT * FROM edges WHERE target = ?;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Search Node By ID&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/search-node-by-id.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

searchNodeById: 
    SELECT body FROM nodes 
    WHERE id = ?;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Search Node&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/search-node.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

-- Create a text index of entries, see https://www.sqlite.org/fts5.html#external_content_tables
CREATE VIRTUAL TABLE node_entries USING fts5 (
    body,
    content=nodes,
    content_rowid=id
);

-- Triggers to keep entries and fts5 index in sync.
CREATE TRIGGER nodes_insert AFTER INSERT ON nodes BEGIN
  INSERT INTO node_entries(rowid, body) VALUES (new.id, new.body);
END;

CREATE TRIGGER nodes_delete AFTER DELETE ON nodes BEGIN
  INSERT INTO node_entries(node_entries, rowid, body) VALUES (&apos;delete&apos;, old.id, old.body);
END;

CREATE TRIGGER nodes_update AFTER UPDATE ON nodes BEGIN
  INSERT INTO node_entries(node_entries, rowid, body) VALUES (&apos;delete&apos;, new.id, new.body);
  INSERT INTO node_entries(rowid, body) VALUES (new.id, new.body);
END;

-- Full text search query.
searchNode: SELECT r.** FROM node_entries
    INNER JOIN nodes r ON r.id = node_entries.rowid
    WHERE node_entries MATCH :query
    ORDER BY rank;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Here we are using the &lt;a href=&quot;https://www.sqlite.org/fts5.html&quot;&gt;fts5&lt;/a&gt; extension to create a full text search index. This is a very powerful feature that allows us to search for nodes by their body text.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Traverse Inbound&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/traverse-inbound.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

traverseInbound(:source AS TEXT): 
  WITH RECURSIVE traverse(id) AS (
  SELECT :source
  UNION
  SELECT source FROM edges JOIN traverse ON target = id
) SELECT id FROM traverse;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Traverse Outbound&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/traverse-outbound.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

traverseOutbound(:source AS TEXT): 
  WITH RECURSIVE traverse(id) AS (
  SELECT :source
  UNION
  SELECT target FROM edges JOIN traverse ON source = id
) SELECT id FROM traverse;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Traverse Bodies Inbound&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/traverse-with-bodies-inbound.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

traverseWithBodiesInbound(:source AS TEXT): 
  WITH RECURSIVE traverse(x, y, obj) AS (
  SELECT :source, &apos;()&apos;, &apos;{}&apos;
  UNION
  SELECT id, &apos;()&apos;, body FROM nodes JOIN traverse ON id = x
  UNION
  SELECT source, &apos;&amp;lt;-&apos;, properties FROM edges JOIN traverse ON target = x
) SELECT x, y, obj FROM traverse;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Traverse Bodies Outbound&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/traverse-with-bodies-outbound.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

traverseWithBodiesOutbound(:source AS TEXT): 
  WITH RECURSIVE traverse(x, y, obj) AS (
  SELECT :source, &apos;()&apos;, &apos;{}&apos;
  UNION
  SELECT id, &apos;()&apos;, body FROM nodes JOIN traverse ON id = x
  UNION
  SELECT target, &apos;-&amp;gt;&apos;, properties FROM edges JOIN traverse ON source = x
) SELECT x, y, obj FROM traverse;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Traverse Bodies&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/traverse-bodies.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

traverseWithBodies(:source AS TEXT): 
  WITH RECURSIVE traverse(x, y, obj) AS (
  SELECT :source, &apos;()&apos;, &apos;{}&apos;
  UNION
  SELECT id, &apos;()&apos;, body FROM nodes JOIN traverse ON id = x
  UNION
  SELECT source, &apos;&amp;lt;-&apos;, properties FROM edges JOIN traverse ON target = x
  UNION
  SELECT target, &apos;-&amp;gt;&apos;, properties FROM edges JOIN traverse ON source = x
) SELECT x, y, obj FROM traverse;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Traverse&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/traverse.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

traverse(:source AS TEXT): 
  WITH RECURSIVE traverse(id) AS (
  SELECT :source
  UNION
  SELECT source FROM edges JOIN traverse ON target = id
  UNION
  SELECT target FROM edges JOIN traverse ON source = id
) SELECT id FROM traverse;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Update Node&lt;/h4&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/sql/update-node.drift&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;import &apos;schema.drift&apos;;

updateNode: 
    UPDATE nodes SET body = json(?) 
    WHERE id = ?;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Database Setup&lt;/h3&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/database/database.dart&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:convert&apos;;

import &apos;package:drift/drift.dart&apos;;
import &apos;package:flutter/foundation.dart&apos;;

import &apos;connection/connection.dart&apos; as impl;

part &apos;database.g.dart&apos;;

@DriftDatabase(include: {
  &apos;sql/schema.drift&apos;,
  &apos;sql/queries.drift&apos;,
  &apos;sql/delete-edge.drift&apos;,
  &apos;sql/delete-node.drift&apos;,
  &apos;sql/insert-edge.drift&apos;,
  &apos;sql/insert-node.drift&apos;,
  &apos;sql/search-edges-inbound.drift&apos;,
  &apos;sql/search-edges-outbound.drift&apos;,
  &apos;sql/search-edges.drift&apos;,
  &apos;sql/search-node-by-id.drift&apos;,
  &apos;sql/search-node.drift&apos;,
  &apos;sql/traverse-inbound.drift&apos;,
  &apos;sql/traverse-outbound.drift&apos;,
  &apos;sql/traverse-with-bodies-inbound.drift&apos;,
  &apos;sql/traverse-with-bodies-outbound.drift&apos;,
  &apos;sql/traverse-with-bodies.drift&apos;,
  &apos;sql/traverse.drift&apos;,
  &apos;sql/update-node.drift&apos;,
})
class GraphDatabase extends _$GraphDatabase {
  GraphDatabase({
    String dbName = &apos;graph_db.db&apos;,
    DatabaseConnection? connection,
    bool useWebWorker = false,
    bool logStatements = false,
  }) : super.connect(
          connection ??
              impl.connect(
                dbName,
                useWebWorker: useWebWorker,
                logStatements: logStatements,
              ),
        );

  @override
  int get schemaVersion =&amp;gt; 1;

  /// Helper method to add graph data from json
  Future&amp;lt;void&amp;gt; addGraphData(
    Map&amp;lt;String, dynamic&amp;gt; data, {
    bool shouldBatch = false,
  }) {
    return transaction(() async {
      try {
        final localNodes = data[&apos;nodes&apos;] as List&amp;lt;dynamic&amp;gt;;
        final localEdges = data[&apos;edges&apos;] as List&amp;lt;dynamic&amp;gt;;
        // Update nodes
        for (final node in localNodes) {
          final id = node[&apos;id&apos;] as String?;
          if (id != null) {
            final current = await searchNodeById(id).getSingleOrNull();
            final body = jsonEncode(node);
            if (current != null) {
              await updateNode(id, body);
            } else {
              await insertNode(body);
            }
          }
        }
        // Update edges
        for (final edge in localEdges) {
          final source = edge[&apos;from&apos;] ?? edge[&apos;source&apos;] as String?;
          final target = edge[&apos;to&apos;] ?? edge[&apos;target&apos;] as String?;
          if (source != null &amp;amp;&amp;amp; target != null) {
            final body = jsonEncode(edge);
            await insertEdge(source, target, body);
          }
        }
      } catch (e) {
        debugPrint(&apos;Error adding graph data: $e&apos;);
      }
    });
  }

  Future&amp;lt;void&amp;gt; deleteAll() {
    return transaction(() async {
      try {
        await deleteAllEdges();
        await deleteAllNodes();
      } catch (e) {
        debugPrint(&apos;Error clearing graph data: $e&apos;);
      }
    });
  }

  Future&amp;lt;void&amp;gt; deleteAllEdges() {
    return transaction(() async {
      final edges = await getAllEdges().get();
      for (final edge in edges) {
        await deleteEdge(edge.source, edge.target);
      }
    });
  }

  Future&amp;lt;void&amp;gt; deleteAllNodes() {
    return transaction(() async {
      final nodes = await getAllNodes().get();
      for (final node in nodes) {
        await deleteNode(node.id);
      }
    });
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;build.yaml&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;targets:
  $default:
    sources:
      - lib/**
      - web/**
      - &amp;quot;tool/**&amp;quot;
      - pubspec.yaml
      - lib/$lib$
      - $package$
    builders:
      drift_dev:
        options:
          sql:
            dialect: sqlite
            options:
              version: &amp;quot;3.38&amp;quot;
              modules:
                - json1
                - fts5
          generate_connect_constructor: true
          apply_converters_on_variables: true
          generate_values_in_copy_with: true
          scoped_dart_components: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now run the following command to generate the database files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter pub run build_runner build --delete-conflicting-outputs
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Connecting to the Database&lt;/h2&gt;
&lt;p&gt;Add a new dependency to your &lt;strong&gt;pubspec.yaml&lt;/strong&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter pub add graphview
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will be used for the graph visualization.&lt;/p&gt;
&lt;p&gt;Create a new file at &lt;strong&gt;lib/main.dart&lt;/strong&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:convert&apos;;

import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter_graph_database/flutter_graph_database.dart&apos; as db;
import &apos;package:graphview/GraphView.dart&apos;;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Graph Database&apos;,
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark(),
      home: const Example(),
    );
  }
}

class Example extends StatefulWidget {
  const Example({Key? key}) : super(key: key);

  @override
  State&amp;lt;Example&amp;gt; createState() =&amp;gt; _ExampleState();
}

class _ExampleState extends State&amp;lt;Example&amp;gt; {
  final database = db.GraphDatabase();
  Graph graph = Graph();
  Algorithm builder = FruchtermanReingoldAlgorithm();

  final nodes = &amp;lt;String, db.Node&amp;gt;{};
  bool loaded = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) =&amp;gt; loadData());
  }

  @override
  void reassemble() {
    super.reassemble();
    // Needed to reset graph on hot reload
    loadData();
  }

  void setLoadedState(bool value) {
    if (mounted) {
      setState(() {
        loaded = value;
      });
    }
  }

  Future&amp;lt;void&amp;gt; addDummyData() async {
    // Load example data
    try {
      // Optionally reset data
      await database.deleteAll();
      // Add example data to database
      await database.addGraphData({
        &amp;quot;nodes&amp;quot;: [
          {&amp;quot;id&amp;quot;: &apos;1&apos;, &amp;quot;label&amp;quot;: &apos;circle&apos;},
          {&amp;quot;id&amp;quot;: &apos;2&apos;, &amp;quot;label&amp;quot;: &apos;ellipse&apos;},
          {&amp;quot;id&amp;quot;: &apos;3&apos;, &amp;quot;label&amp;quot;: &apos;database&apos;},
          {&amp;quot;id&amp;quot;: &apos;4&apos;, &amp;quot;label&amp;quot;: &apos;box&apos;},
          {&amp;quot;id&amp;quot;: &apos;5&apos;, &amp;quot;label&amp;quot;: &apos;diamond&apos;},
          {&amp;quot;id&amp;quot;: &apos;6&apos;, &amp;quot;label&amp;quot;: &apos;dot&apos;},
          {&amp;quot;id&amp;quot;: &apos;7&apos;, &amp;quot;label&amp;quot;: &apos;square&apos;},
          {&amp;quot;id&amp;quot;: &apos;8&apos;, &amp;quot;label&amp;quot;: &apos;triangle&apos;},
          {&amp;quot;id&amp;quot;: &apos;9&apos;, &amp;quot;label&amp;quot;: &amp;quot;star&amp;quot;},
        ],
        &amp;quot;edges&amp;quot;: [
          {&amp;quot;from&amp;quot;: &apos;1&apos;, &amp;quot;to&amp;quot;: &apos;2&apos;},
          {&amp;quot;from&amp;quot;: &apos;2&apos;, &amp;quot;to&amp;quot;: &apos;3&apos;},
          {&amp;quot;from&amp;quot;: &apos;2&apos;, &amp;quot;to&amp;quot;: &apos;4&apos;},
          {&amp;quot;from&amp;quot;: &apos;2&apos;, &amp;quot;to&amp;quot;: &apos;5&apos;},
          {&amp;quot;from&amp;quot;: &apos;5&apos;, &amp;quot;to&amp;quot;: &apos;6&apos;},
          {&amp;quot;from&amp;quot;: &apos;5&apos;, &amp;quot;to&amp;quot;: &apos;7&apos;},
          {&amp;quot;from&amp;quot;: &apos;6&apos;, &amp;quot;to&amp;quot;: &apos;8&apos;},
          {&amp;quot;from&amp;quot;: &apos;2&apos;, &amp;quot;to&amp;quot;: &apos;8&apos;},
          {&amp;quot;from&amp;quot;: &apos;1&apos;, &amp;quot;to&amp;quot;: &apos;8&apos;},
          {&amp;quot;from&amp;quot;: &apos;1&apos;, &amp;quot;to&amp;quot;: &apos;7&apos;},
          {&amp;quot;from&amp;quot;: &apos;1&apos;, &amp;quot;to&amp;quot;: &apos;6&apos;},
          {&amp;quot;from&amp;quot;: &apos;1&apos;, &amp;quot;to&amp;quot;: &apos;5&apos;},
          {&amp;quot;from&amp;quot;: &apos;1&apos;, &amp;quot;to&amp;quot;: &apos;4&apos;},
          {&amp;quot;from&amp;quot;: &apos;1&apos;, &amp;quot;to&amp;quot;: &apos;3&apos;},
          {&amp;quot;from&amp;quot;: &apos;1&apos;, &amp;quot;to&amp;quot;: &apos;9&apos;},
          {&amp;quot;from&amp;quot;: &apos;9&apos;, &amp;quot;to&amp;quot;: &apos;8&apos;},
          {&amp;quot;from&amp;quot;: &apos;9&apos;, &amp;quot;to&amp;quot;: &apos;5&apos;},
          {&amp;quot;from&amp;quot;: &apos;9&apos;, &amp;quot;to&amp;quot;: &apos;3&apos;},
        ]
      });
      loadData();
    } catch (e) {
      debugPrint(&apos;Error loading example data: $e&apos;);
    }
  }

  Future&amp;lt;void&amp;gt; loadData() async {
    setLoadedState(false);

    final nodeMap = &amp;lt;String, Node&amp;gt;{};
    this.nodes.clear();
    graph = Graph();
    builder = FruchtermanReingoldAlgorithm();

    // Load graph data
    final nodes = await database.getAllNodes().get();
    final edges = await database.getAllEdges().get();

    for (final node in nodes) {
      final newNode = Node.Id(node.id);
      nodeMap[node.id] = newNode;
      this.nodes[node.id] = node;
      graph.addNode(newNode);
    }
    for (final edge in edges) {
      final source = nodeMap[edge.source];
      final target = nodeMap[edge.target];
      if (source != null &amp;amp;&amp;amp; target != null) {
        graph.addEdge(source, target);
      }
    }

    setLoadedState(true);
  }

  Widget buildNode(Node node) {
    final dbNode = nodes[node.key!.value];
    final data = jsonDecode(dbNode?.body ?? &apos;{}&apos;) as Map&amp;lt;String, dynamic&amp;gt;;
    final label = data[&apos;label&apos;] ?? &apos;&apos;;
    return SizedBox(
      width: 80,
      height: 80,
      child: Center(
        child: Text(
          label,
          textAlign: TextAlign.center,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(&apos;Flutter Graph Database&apos;),
        actions: [
          IconButton(
            icon: const Icon(Icons.restore),
            onPressed: addDummyData,
          ),
        ],
      ),
      body: !loaded
          ? const Center(child: CircularProgressIndicator())
          : nodes.isEmpty
              ? const Center(child: Text(&apos;No Data Loaded&apos;))
              : LayoutBuilder(builder: (context, dimens) {
                  return SizedBox.expand(
                    child: InteractiveViewer(
                      constrained: false,
                      boundaryMargin: EdgeInsets.symmetric(
                        horizontal: dimens.maxWidth * 0.75,
                        vertical: dimens.maxHeight * 0.75,
                      ),
                      minScale: 0.01,
                      maxScale: 5.6,
                      child: GraphView(
                        key: UniqueKey(),
                        graph: graph,
                        algorithm: builder,
                        paint: Paint()
                          ..color = Colors.green
                          ..strokeWidth = 1
                          ..style = PaintingStyle.stroke,
                        builder: buildNode,
                      ),
                    ),
                  );
                }),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run the flutter app you should see the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/graph_flutter_final_yikh8roctq.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to learn more about building a graph database in Flutter, check out the &lt;a href=&quot;https://github.com/rodydavis/flutter_graph_database&quot;&gt;source code&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Host your Flutter Project as a REST API</title><link>https://rodydavis.com/flutter/host-rest-api/</link><guid isPermaLink="true">https://rodydavis.com/flutter/host-rest-api/</guid><description>This tutorial demonstrates how to create a single Flutter project that serves as both a client application and a REST API, enabling code reuse and simplifying data management, deployable to platforms like Google Cloud Run.</description><pubDate>Mon, 20 Jan 2025 00:30:40 GMT</pubDate><content:encoded>&lt;h1&gt;Host your Flutter Project as a REST API&lt;/h1&gt;
&lt;p&gt;After you build your flutter project you may want to reuse the models and business logic from your lib folder. I will show you how to go about setting up the project to have iOS, Android, Web, Windows, MacOS, Linux and a REST API interface with one project. The REST API can also be deploy to Google Cloud Run for Dart everywhere.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_1_gl7e2erkta.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One Codebase for Client and Sever.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This will allow you to expose your Dart models as a REST API and run your business logic from your lib folder while the application runs the models as they are. &lt;a href=&quot;https://github.com/rodydavis/shared_dart&quot;&gt;Here&lt;/a&gt; is the final project.&lt;/p&gt;
&lt;h2&gt;Setting Up&lt;/h2&gt;
&lt;p&gt;As with any Flutter project I am going to assume that you already have &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt; installed on your machine and that you can create a project. This is a intermediate level difficulty so read on if you are up to the challenge. You will also need to know the basics of &lt;a href=&quot;https://www.docker.com/&quot;&gt;Docker&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Why one project?&lt;/h2&gt;
&lt;p&gt;It may not be obvious but when building complex applications you will at some point have a server and an application that calls that server. &lt;a href=&quot;https://firebase.google.com/&quot;&gt;Firebase&lt;/a&gt; is an excellent option for doing this and I use it in almost all my projects. &lt;a href=&quot;https://firebase.google.com/products/functions/&quot;&gt;Firebase Functions&lt;/a&gt; are really powerful but you are limited by Javascript or Typescript. What if you could use the same packages that you are using in the Flutter project, or better yet what if they both used the same?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_2_t53hqci5ox.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;When you have a server project and a client project that communicate over a rest api or client sdk like Firebase then you will run into the problem that the server has models of objects stored and the client has models of the objects that are stored. This can lead to a serious mismatch when it changed without you knowing. GraphQL helps a lot with this since you define the model that you recieve. This approach allows your business logic to be always up to date for both the client and server.&lt;/p&gt;
&lt;h2&gt;Client Setup&lt;/h2&gt;
&lt;p&gt;The first step is to just build your application. The only difference that we will make is keeping the UI and business logic separate. When starting out with Flutter it can be very easy to throw all the logic into the screen and calling setState when the data changes. Even the application when creating a new Flutter project does this. That&apos;s why &lt;a href=&quot;https://flutter.dev/docs/development/data-and-backend/state-mgmt/options&quot;&gt;choosing a state management solution&lt;/a&gt; is so important.&lt;/p&gt;
&lt;p&gt;To make things clean and concise we will make 2 folders in our lib folder.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ui for all Flutter Widgets and Screens&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;src for all business logic, classes, models and utility functions&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This will leave us with main.dart being only the entry point into our client application.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;plugins/desktop/desktop.dart&apos;;
import &apos;ui/home/screen.dart&apos;;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      home: HomeScreen(),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s Start by making a tab bar for the 2 screens. Create a file in the folder ui/home/screen.dart and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;../counter/screen.dart&apos;;
import &apos;../todo/screen.dart&apos;;

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() =&amp;gt; _HomeScreenState();
}

class _HomeScreenState extends State&amp;lt;HomeScreen&amp;gt; {
  int _currentIndex = 0;

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: &amp;lt;Widget&amp;gt;[
          CounterScreen(),
          TodosScreen(),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (val) {
          if (mounted)
            setState(() {
              _currentIndex = val;
            });
        },
        type: BottomNavigationBarType.fixed,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.add),
            title: Text(&apos;Counter&apos;),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.list),
            title: Text(&apos;Todos&apos;),
          ),
        ],
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is just a basic screen and should look very normal.&lt;/p&gt;
&lt;h3&gt;Counter Example&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_3_pwj25p3nfr.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now create a file ui/counter/screen.dart and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:shared_dart/src/models/counter.dart&apos;;

class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() =&amp;gt; _CounterScreenState();
}

class _CounterScreenState extends State&amp;lt;CounterScreen&amp;gt; {
  CounterModel _counterModel = CounterModel();

void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counterModel.add();
    });
  }

@override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyCounterPage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(&apos;Counter Screen&apos;),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke &amp;quot;debug painting&amp;quot; (press &amp;quot;p&amp;quot; in the console, choose the
          // &amp;quot;Toggle Debug Paint&amp;quot; action from the Flutter Inspector in Android
          // Studio, or the &amp;quot;Toggle Debug Paint&amp;quot; command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: &amp;lt;Widget&amp;gt;[
            Text(
              &apos;You have pushed the button this many times:&apos;,
            ),
            Text(
              &apos;${_counterModel.count}&apos;,
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: &apos;Increment&apos;,
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the default counter app you get when you create a Flutter application but with one change, it uses &lt;code&gt;CounterModel&lt;/code&gt; to hold the logic.&lt;/p&gt;
&lt;p&gt;Create the counter model at src/models/counter.dart and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class CounterModel {
  CounterModel();

  int _count = 0;

  int get count =&amp;gt; _count;

  void add() =&amp;gt; _count++;

  void subtract() =&amp;gt; _count--;

  void set(int val) =&amp;gt; _count = val;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see it is really easy to expose only what we want to while still having complete flexibility. You could use provider here if you choose, or even bloc and/or streams.&lt;/p&gt;
&lt;h3&gt;Todo Example&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_4_4cia0ajhj0.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Lets create a file at ui/todos/screen.dart and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;../../src/classes/todo.dart&apos;;
import &apos;../../src/models/todos.dart&apos;;

class TodosScreen extends StatefulWidget {
  @override
  _TodosScreenState createState() =&amp;gt; _TodosScreenState();
}

class _TodosScreenState extends State&amp;lt;TodosScreen&amp;gt; {
  final _model = TodosModel();
  List&amp;lt;ToDo&amp;gt; _todos;

@override
  void initState() {
    _model.getList().then((val) {
      if (mounted)
        setState(() {
          _todos = val;
        });
    });
    super.initState();
  }

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(&apos;Todos Screen&apos;),
      ),
      body: Builder(
        builder: (_) {
          if (_todos != null) {
            return ListView.builder(
              itemCount: _todos.length,
              itemBuilder: (context, index) {
                final _item = _todos[index];
                return ListTile(
                  title: Text(_item.title),
                  subtitle: Text(_item.completed ? &apos;Completed&apos; : &apos;Pending&apos;),
                );
              },
            );
          }
          return Center(
            child: CircularProgressIndicator(),
          );
        },
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will see that we have the logic in TodosModel and uses the class ToDo for toJson and fromJson.&lt;/p&gt;
&lt;p&gt;Create a file at the location src/classes/todo.dart and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// To parse this JSON data, do
//
//     final toDo = toDoFromJson(jsonString);

import &apos;dart:convert&apos;;

List&amp;lt;ToDo&amp;gt; toDoFromJson(String str) =&amp;gt; List&amp;lt;ToDo&amp;gt;.from(json.decode(str).map((x) =&amp;gt; ToDo.fromJson(x)));

String toDoToJson(List&amp;lt;ToDo&amp;gt; data) =&amp;gt; json.encode(List&amp;lt;dynamic&amp;gt;.from(data.map((x) =&amp;gt; x.toJson())));

class ToDo {
    int userId;
    int id;
    String title;
    bool completed;

ToDo({
        this.userId,
        this.id,
        this.title,
        this.completed,
    });

factory ToDo.fromJson(Map&amp;lt;String, dynamic&amp;gt; json) =&amp;gt; ToDo(
        userId: json[&amp;quot;userId&amp;quot;],
        id: json[&amp;quot;id&amp;quot;],
        title: json[&amp;quot;title&amp;quot;],
        completed: json[&amp;quot;completed&amp;quot;],
    );

Map&amp;lt;String, dynamic&amp;gt; toJson() =&amp;gt; {
        &amp;quot;userId&amp;quot;: userId,
        &amp;quot;id&amp;quot;: id,
        &amp;quot;title&amp;quot;: title,
        &amp;quot;completed&amp;quot;: completed,
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and create the model src/models/todo.dart and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:convert&apos;;

import &apos;package:http/http.dart&apos; as http;
import &apos;package:shared_dart/src/classes/todo.dart&apos; as t;

class TodosModel {
  final kTodosUrl = &apos;[https://jsonplaceholder.typicode.com/todos&apos;](https://jsonplaceholder.typicode.com/todos&apos;);

Future&amp;lt;List&amp;lt;t.ToDo&amp;gt;&amp;gt; getList() async {
    final _response = await http.get(kTodosUrl);
    if (_response != null) {
      final _todos = t.toDoFromJson(_response.body);
      if (_todos != null) {
        return _todos;
      }
    }
    return [];
  }

Future&amp;lt;t.ToDo&amp;gt; getItem(int id) async {
    final _response = await http.get(&apos;$kTodosUrl/$id&apos;);
    if (_response != null) {
      final _todo = t.ToDo.fromJson(json.decode(_response.body));
      if (_todo != null) {
        return _todo;
      }
    }
    return null;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we just get dummy data from a url that emits json and convert them to our classes. This is an example I want to show with networking. There is only one place that fetches the data.&lt;/p&gt;
&lt;h3&gt;Run the Project (Web)&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_5_cpppxwnavj.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_6_7bhqlwxjfs.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;As you can see when you run your project on chrome you will get the same application that you got on mobile. Even the networking is working in the web. You can call the model and retrieve the list just like you would expect.&lt;/p&gt;
&lt;h2&gt;Server Setup&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Now time for the magic..&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the root of the project folder create a file Dockerfile and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# Use Google&apos;s official Dart image.
# [https://hub.docker.com/r/google/dart-runtime/](https://hub.docker.com/r/google/dart-runtime/)
FROM google/dart-runtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create another file at the root called service.yaml and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;apiVersion: serving.knative.dev/v1
    kind: Service
    metadata:
      name: PROJECT_NAME
      namespace: default
    spec:
      template:
        spec:
          containers:
            - image: docker.io/YOUR_DOCKER_NAME/PROJECT_NAME
              env:
                - name: TARGET
                  value: &amp;quot;PROJECT_NAME v1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace PROJECT_NAME with your project name, mine is shared-dart for this example.&lt;/p&gt;
&lt;p&gt;You will also need to replace YOUR_DOCKER_NAME with your docker username so the container can be deployed correctly.&lt;/p&gt;
&lt;p&gt;Update your pubspec.yaml with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;name: shared_dart
description: A new Flutter project.
publish_to: none
version: 1.0.0+1

environment:
  sdk: &amp;quot;&amp;gt;=2.1.0 &amp;lt;3.0.0&amp;quot;

dependencies:
  flutter:
    sdk: flutter
  shelf: ^0.7.3
  cupertino_icons: ^0.1.2
  http: ^0.12.0+2

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important package here is shelf as it allows us to run a http server with dart.&lt;/p&gt;
&lt;p&gt;Create a folder in the root of the project called bin then add a file server.dart and replace it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:io&apos;;

import &apos;package:shelf/shelf.dart&apos; as shelf;
import &apos;package:shelf/shelf_io.dart&apos; as io;

import &apos;src/routing.dart&apos;;

void main() {
  final handler = const shelf.Pipeline()
      .addMiddleware(shelf.logRequests())
      .addHandler(RouteUtils.handler);

final port = int.tryParse(Platform.environment[&apos;PORT&apos;] ?? &apos;8080&apos;);
  final address = InternetAddress.anyIPv4;

io.serve(handler, address, port).then((server) {
    server.autoCompress = true;
    print(&apos;Serving at [http://${server.address.host}:${server.port}&apos;](http://${server.address.host}:${server.port}&apos;));
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will tell the container what port to listen for and how to handle the requests.&lt;/p&gt;
&lt;p&gt;Create a folder src in the bin folder and add a file routing.dart and replace the contents with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:async&apos;;

import &apos;package:shelf/shelf.dart&apos; as shelf;

import &apos;controllers/index.dart&apos;;
import &apos;result.dart&apos;;

class RouteUtils {
  static FutureOr&amp;lt;shelf.Response&amp;gt; handler(shelf.Request request) {
    var component = request.url.pathSegments.first;
    var handler = _handlers(request)[component];
    if (handler == null) return shelf.Response.notFound(null);
    return handler;
  }

static Map&amp;lt;String, FutureOr&amp;lt;shelf.Response&amp;gt;&amp;gt; _handlers(
      shelf.Request request) {
    return {
      &apos;info&apos;: ServerResponse(&apos;Info&apos;, body: {
        &amp;quot;version&amp;quot;: &apos;v1.0.0&apos;,
        &amp;quot;status&amp;quot;: &amp;quot;ok&amp;quot;,
      }).ok(),
      &apos;counter&apos;: CounterController().result(request),
      &apos;todos&apos;: TodoController().result(request),
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is still nothing imported from our main project but you will start to see some similarities. Here we specify controllers for todos and counter url paths.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;&apos;counter&apos;: CounterController().result(request),
&apos;todos&apos;: TodoController().result(request),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;that means any url with the following:&lt;a href=&quot;https://mydomain.com/todos&quot;&gt;https://mydomain.com/todos&lt;/a&gt; , &lt;a href=&quot;https://mydomain.com/todos&quot;&gt;https://mydomain.com/todos&lt;/a&gt;/1&lt;/p&gt;
&lt;p&gt;will get routed to the TodoController to handle the request.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is also the first time I found out about FutureOr. It allows you to return a sync or async function.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And important part about build a REST API is having a consistent response body, so here we can create a wrapper that adds fields we always want to return, like the status of the call, a message and the body.&lt;/p&gt;
&lt;p&gt;Create a file at src/result.dart and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:convert&apos;;

import &apos;package:shelf/shelf.dart&apos; as shelf;

class ServerResponse {
  final String message;
  final dynamic body;
  final StatusType type;

ServerResponse(
    this.message, {
    this.type = StatusType.success,
    this.body,
  });

Map&amp;lt;String, dynamic&amp;gt; toJson() {
    return {
      &amp;quot;status&amp;quot;: type.toString().replaceAll(&apos;StatusType.&apos;, &apos;&apos;),
      &amp;quot;message&amp;quot;: message,
      &amp;quot;body&amp;quot;: body ?? &apos;&apos;,
    };
  }

String toJsonString() {
    return json.encode(toJson());
  }

shelf.Response ok() {
    return shelf.Response.ok(
      toJsonString(),
      headers: {
        &apos;Content-Type&apos;: &apos;application/json&apos;,
      },
    );
  }
}

enum StatusType { success, error }

abstract class ResponseImpl {
  Future&amp;lt;shelf.Response&amp;gt; result(shelf.Request request);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will always return json and the fields that we want to show. You could also include your paging meta data here.&lt;/p&gt;
&lt;p&gt;Create a file in at the location src/controllers/counter.dart and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:shared_dart/src/models/counter.dart&apos;;
import &apos;package:shelf/shelf.dart&apos; as shelf;

import &apos;../result.dart&apos;;

class CounterController implements ResponseImpl {
  const CounterController();

@override
  Future&amp;lt;shelf.Response&amp;gt; result(shelf.Request request) async {
    final _model = CounterModel();
    final _params = request.url.queryParameters;
    if (_params != null) {
      final _val = int.tryParse(_params[&apos;count&apos;] ?? &apos;0&apos;);
      _model.set(_val);
    } else {
      _model.add();
    }
    return ServerResponse(&apos;Info&apos;, body: {
      &amp;quot;counter&amp;quot;: _model.count,
    }).ok();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will see the import to the lib folder of the root project. Since it shares the pubspec.yaml all the packages can be shared. You can import the CounterModel that we created earlier.&lt;/p&gt;
&lt;p&gt;Create a file in at the location src/controllers/todos.dart and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:shared_dart/src/models/todos.dart&apos;;
import &apos;package:shelf/src/request.dart&apos;;

import &apos;package:shelf/src/response.dart&apos;;

import &apos;../result.dart&apos;;

class TodoController implements ResponseImpl {
  @override
  Future&amp;lt;Response&amp;gt; result(Request request) async {
    final _model = TodosModel();
    if (request.url.pathSegments.length &amp;gt; 1) {
      final _id = int.tryParse(request.url.pathSegments[1] ?? &apos;1&apos;);
      final _todo = await _model.getItem(_id);
      return ServerResponse(&apos;Todo Item&apos;, body: _todo).ok();
    }
    final _todos = await _model.getList();
    return ServerResponse(
      &apos;List Todos&apos;,
      body: _todos.map((t) =&amp;gt; t.toJson()).toList(),
    ).ok();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just like before we are importing the TodosModel model from the lib folder.&lt;/p&gt;
&lt;p&gt;For convenience add a file at the location src/controllers/index.dart and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;export &apos;counter.dart&apos;;
export &apos;todo.dart&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will make it easier to import all the controllers.&lt;/p&gt;
&lt;h2&gt;Run the Project (Server)&lt;/h2&gt;
&lt;p&gt;If you are using &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VSCode&lt;/a&gt; then you will need to update your launch.json with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: [https://go.microsoft.com/fwlink/?linkid=830387](https://go.microsoft.com/fwlink/?linkid=830387)
    &amp;quot;version&amp;quot;: &amp;quot;0.2.0&amp;quot;,
    &amp;quot;configurations&amp;quot;: [
        {
            &amp;quot;name&amp;quot;: &amp;quot;Client&amp;quot;,
            &amp;quot;request&amp;quot;: &amp;quot;launch&amp;quot;,
            &amp;quot;type&amp;quot;: &amp;quot;dart&amp;quot;,
            &amp;quot;program&amp;quot;: &amp;quot;lib/main.dart&amp;quot;
        }, 
          {
            &amp;quot;name&amp;quot;: &amp;quot;Server&amp;quot;,
            &amp;quot;request&amp;quot;: &amp;quot;launch&amp;quot;,
            &amp;quot;type&amp;quot;: &amp;quot;dart&amp;quot;,
            &amp;quot;program&amp;quot;: &amp;quot;bin/server.dart&amp;quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when you hit run with Server selected you will see the output:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_7_fph074ovtl.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can navigate to this in a browser but you can also work with this in &lt;a href=&quot;https://www.getpostman.com/&quot;&gt;Postman&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_8_3jwm0aouc0.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_9_ry6xjpbpbx.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Just by adding to the url todos and todos/1 it will return different responses.&lt;/p&gt;
&lt;p&gt;For the counter model we can use query parameters too!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_10_ec9e6yhbxc.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_11_puha19uiln.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Just by adding ?count=22 it will update the model with the input.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Keep in mind this is running your Dart code from you lib folder in your Flutter project without needing the Flutter widgets!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As a side benefit we can also run this project on Desktop. Check out the final project for the desktop folders needed from &lt;a href=&quot;https://github.com/google/flutter-desktop-embedding&quot;&gt;Flutter Desktop Embedding&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_12_2rv9jnqeo0.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_13_k7amx8s1d5.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Now if you wanted to deploy the container to Cloud Run you could with the following command:&lt;/p&gt;
&lt;p&gt;gcloud builds submit — tag gcr.io/YOUR_GOOGLE_PROJECT_ID/PROJECT_NAME .&lt;/p&gt;
&lt;p&gt;Replace PROJECT_NAME with your project name, mine is shared-dart for this example.&lt;/p&gt;
&lt;p&gt;You will also need to replace YOUR_GOOGLE_PROJECT_ID with your Google Cloud Project ID. You can create one &lt;a href=&quot;https://cloud.google.com/cloud-build/docs/quickstart-docker&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Again the final project source code is &lt;a href=&quot;https://github.com/rodydavis/shared_dart&quot;&gt;here&lt;/a&gt;. Let me know your thoughts!&lt;/p&gt;
</content:encoded></item><item><title>Lit and Flutter</title><link>https://rodydavis.com/flutter/lit-interop/</link><guid isPermaLink="true">https://rodydavis.com/flutter/lit-interop/</guid><description>Embed a Lit web component in a Flutter application to access device APIs and create a cross-platform app that updates automatically with website changes.</description><pubDate>Mon, 20 Jan 2025 02:22:00 GMT</pubDate><content:encoded>&lt;h1&gt;Lit and Flutter&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to set up a &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; web component and use it inline in the Flutter widget tree.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; You can find the final source &lt;a href=&quot;https://github.com/rodydavis/flutter_hybrid_template&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The reason you would want this integration is so you can take an existing web app, or just a single part of it and embed it in the widget tree.&lt;/p&gt;
&lt;p&gt;With it wrapped in Flutter you can call device APIs from event listeners on your web component.&lt;/p&gt;
&lt;p&gt;For example you may have an app that handles purchases, and now you can call the in app purchase API or other device specific features not available on the web.&lt;/p&gt;
&lt;p&gt;You also get a cross platform app that can be delivered to both Google Play and the App Store.&lt;/p&gt;
&lt;p&gt;The web component will receive new code each time you update your site, so you do not have to ship an update each time the web component changes.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Flutter SDK&lt;/li&gt;
&lt;li&gt;Xcode and Command Line Tools&lt;/li&gt;
&lt;li&gt;Android SDK&lt;/li&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Node&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by creating a empty directory and naming it with &lt;code&gt;snake_case&lt;/code&gt; whatever we want.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir flutter_lit_example
cd flutter_lit_example
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Web Setup&lt;/h3&gt;
&lt;p&gt;Now we are in the &lt;code&gt;flutter_lit_example&lt;/code&gt; directory and can setup Flutter and Lit. Let&apos;s start with node.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init -y
npm i lit
npm i -D typescript vite @types/node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will setup the basics for a node project and install the packages we need. Now lets add some config files.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;touch tsconfig.json
touch vite.config.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create 2 files. Now open up &lt;code&gt;tsconfig.json&lt;/code&gt; and paste the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &amp;quot;compilerOptions&amp;quot;: {
    &amp;quot;module&amp;quot;: &amp;quot;esnext&amp;quot;,
    &amp;quot;lib&amp;quot;: [
      &amp;quot;es2017&amp;quot;,
      &amp;quot;dom&amp;quot;,
      &amp;quot;dom.iterable&amp;quot;
    ],
    &amp;quot;types&amp;quot;: [
      &amp;quot;vite/client&amp;quot;
    ],
    &amp;quot;declaration&amp;quot;: true,
    &amp;quot;emitDeclarationOnly&amp;quot;: true,
    &amp;quot;outDir&amp;quot;: &amp;quot;./types&amp;quot;,
    &amp;quot;rootDir&amp;quot;: &amp;quot;./src&amp;quot;,
    &amp;quot;strict&amp;quot;: true,
    &amp;quot;noUnusedLocals&amp;quot;: true,
    &amp;quot;noUnusedParameters&amp;quot;: true,
    &amp;quot;noImplicitReturns&amp;quot;: true,
    &amp;quot;noFallthroughCasesInSwitch&amp;quot;: true,
    &amp;quot;moduleResolution&amp;quot;: &amp;quot;node&amp;quot;,
    &amp;quot;allowSyntheticDefaultImports&amp;quot;: true,
    &amp;quot;experimentalDecorators&amp;quot;: true,
    &amp;quot;forceConsistentCasingInFileNames&amp;quot;: true
  },
  &amp;quot;include&amp;quot;: [
    &amp;quot;src/**/*.ts&amp;quot;
  ],
  &amp;quot;exclude&amp;quot;: []
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a basic typescript config. Now open up &lt;code&gt;vite.config.ts&lt;/code&gt; and paste the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from &amp;quot;vite&amp;quot;;
import { resolve } from &amp;quot;path&amp;quot;;

// https://vitejs.dev/config/
export default defineConfig({
  base: &amp;quot;/flutter_lit_example/&amp;quot;, // TODO: Name of your github repo
  build: {
    outDir: &amp;quot;build/web&amp;quot;,
    rollupOptions: {
      output: {
        entryFileNames: `assets/[name].js`,
        chunkFileNames: `assets/[name].js`,
        assetFileNames: `assets/[name].[ext]`,
      },
      input: {
        main: resolve(__dirname, &amp;quot;index.html&amp;quot;),
        // TODO: Create a new module for each component you want to embed
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to create our web component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir src
cd src
touch my-app.ts
cd ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;my-app.ts&lt;/code&gt; and paste the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, css, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, property } from &amp;quot;lit/decorators.js&amp;quot;;

@customElement(&amp;quot;my-app&amp;quot;)
export class MyApp extends LitElement {
  static styles = css`
    p {
      color: blue;
    }
  `;

  @property()
  name = &amp;quot;Somebody&amp;quot;;

  render() {
    return html`&amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;Hello, ${this.name}!&amp;lt;/p&amp;gt;
      &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
    &amp;lt;/div&amp;gt;`;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to create a &lt;code&gt;index.html&lt;/code&gt; for our web app.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;touch index.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;index.html&lt;/code&gt; and paste the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;meta http-equiv=&amp;quot;X-UA-Compatible&amp;quot; content=&amp;quot;IE=edge&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;Example&amp;lt;/title&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/my-app.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        padding: 0;
        margin: 0;
      }
      my-app {
        width: 100%;
        height: 100vh;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;my-app&amp;gt;&amp;lt;/my-app&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Flutter Setup&lt;/h3&gt;
&lt;p&gt;Now that we have the basics setup for web we can move on to flutter. Let&apos;s create the project with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter create --platforms=ios,android .
flutter packages get
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open up &lt;code&gt;pubspec.yaml&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;name: flutter_lit_example
description: A hybrid Flutter app.
publish_to: &amp;quot;none&amp;quot;
version: 1.0.0+1

environment:
  sdk: &amp;quot;&amp;gt;=2.7.0 &amp;lt;3.0.0&amp;quot;

dependencies:
  flutter:
    sdk: flutter
  flutter_inappwebview: ^5.3.2

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to get the packages again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter packages get
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to create the file that will wrap the web component.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd lib
touch web_component.dart
cd ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;web_component.dart&lt;/code&gt; and paste the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter_inappwebview/flutter_inappwebview.dart&apos;;

class WebComponent extends StatefulWidget {
  const WebComponent({
    Key key,
    @required this.name,
    @required this.bundle,
    this.attributes = const {},
    this.slot = &apos;&apos;,
    this.events = const [],
  }) : super(key: key);
  final String name, bundle;
  final Map&amp;lt;String, String&amp;gt; attributes;
  final String slot;
  final List&amp;lt;EventCallback&amp;gt; events;

  @override
  _WebComponentState createState() =&amp;gt; _WebComponentState();
}

class _WebComponentState extends State&amp;lt;WebComponent&amp;gt; {
  InAppWebViewController controller;
  final Map&amp;lt;String, List&amp;lt;EventCallback&amp;gt;&amp;gt; _events = {};

  String get source {
    return &apos;&apos;&apos;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;meta http-equiv=&amp;quot;X-UA-Compatible&amp;quot; content=&amp;quot;IE=edge&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        padding: 0;
        margin: 0;
      }
      ${widget.name} {
        width: 100%;
        height: 100vh;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;script type=&amp;quot;module&amp;quot; crossorigin src=&amp;quot;${widget.bundle}&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;${widget.name} ${widget.attributes.entries.map((e) =&amp;gt; &apos;${e.key}=&amp;quot;${e.value}&amp;quot;&apos;).join(&apos; &apos;)}&amp;gt;
      ${widget.slot}
    &amp;lt;/${widget.name}&amp;gt;
    &amp;lt;script&amp;gt;
    window.addEventListener(&amp;quot;flutterInAppWebViewPlatformReady&amp;quot;, (event) =&amp;gt; {
      ${widget.events.join(&apos;\n&apos;)}
    });
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt; 
&apos;&apos;&apos;;
  }

  void _setup(InAppWebViewController controller) {
    this.controller = controller;
    this._setupEvents();
  }

  void _setupEvents() {
    for (final event in _events.keys) {
      controller.removeJavaScriptHandler(handlerName: event);
    }
    for (final event in widget.events) {
      _addEvent(event);
    }
  }

  void _addEvent(EventCallback event) {
    controller.addJavaScriptHandler(
      handlerName: event.query,
      callback: event.onPressed,
    );
    _events[event.event] ??= [];
    _events[event.event].add(event);
  }

  @override
  void didUpdateWidget(covariant WebComponent oldWidget) {
    if (oldWidget.events != widget.events) {
      _setupEvents();
    }
    if (oldWidget.slot != widget.slot ||
        oldWidget.bundle != widget.bundle ||
        oldWidget.name != widget.name) {
      controller.loadData(data: source);
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return InAppWebView(
      initialData: InAppWebViewInitialData(data: source),
      onWebViewCreated: _setup,
    );
  }
}

class EventCallback {
  EventCallback({
    @required this.onPressed,
    @required this.event,
    this.query,
  });
  final String query, event;
  final dynamic Function(List&amp;lt;dynamic&amp;gt; args) onPressed;

  @override
  String toString() =&amp;gt; _source;

  String get _prefix =&amp;gt; query != null &amp;amp;&amp;amp; query.isNotEmpty
      ? &apos;document.querySelector(&amp;quot;$query&amp;quot;)&apos;
      : &apos;document.body&apos;;

  String get _source =&amp;gt; [
        &apos;$_prefix.addEventListener(&amp;quot;$event&amp;quot;, (e) =&amp;gt; {&apos;,
        &apos;  window.flutter_inappwebview.callHandler(&amp;quot;$query&amp;quot;, e);&apos;,
        &apos;}, false);&apos;,
      ].join(&apos;\n&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;main.dart&lt;/code&gt; and paste it with th following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;web_component.dart&apos;;

const WEBSITE_URL = &apos;https://rodydavis.github.io/flutter_lit_example/&apos;;
const BUNDLE_PATH = &apos;assets/main.js&apos;;

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final title = &apos;Flutter Hybrid App&apos;;
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: title,
      theme: ThemeData(primarySwatch: Colors.blue),
      home: MyHomePage(title: title),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() =&amp;gt; _MyHomePageState();
}

class _MyHomePageState extends State&amp;lt;MyHomePage&amp;gt; {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Builder(
        builder: (context) =&amp;gt; WebComponent(
          name: &apos;my-app&apos;,
          bundle: &apos;$WEBSITE_URL/$BUNDLE_PATH&apos;,
          attributes: {
            &apos;name&apos;: widget.title,
          },
          slot: &apos;&amp;lt;button id=&amp;quot;my-button&amp;quot;&amp;gt;Talk back!&amp;lt;/button&amp;gt;&apos;,
          events: [
            EventCallback(
              event: &apos;click&apos;,
              query: &apos;#my-button&apos;,
              onPressed: (_) {
                ScaffoldMessenger.of(context)
                    .showSnackBar(SnackBar(content: Text(&apos;Clicked!&apos;)));
              },
            ),
          ],
        ),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will need to update &lt;code&gt;WEBSITE_URL&lt;/code&gt; to have the url of the website where you will be deploying and &lt;code&gt;BUNDLE_URL&lt;/code&gt; to the relative path to the js bundle.&lt;/p&gt;
&lt;p&gt;This will ensure auto updates with a new version rolls out and the cache it stale. This will also allow for offline support after the first time it is downloaded.&lt;/p&gt;
&lt;h2&gt;Running&lt;/h2&gt;
&lt;p&gt;Now we can run our application but it requires a few steps to get it all setup.&lt;/p&gt;
&lt;p&gt;To test and build our web app locally we will use &lt;a href=&quot;https://github.com/vitejs/vite&quot;&gt;vite&lt;/a&gt; and render the &lt;code&gt;index.html&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm i
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;vite v2.2.3 dev server running at:

Local:    http://localhost:3000/flutter_lit_example/
Network:  http://192.168.1.143:3000/flutter_lit_example/

ready in 311ms.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can open the link &lt;code&gt;http://localhost:3000/flutter_lit_example/&lt;/code&gt; to see our running web app and hot reload changes from &lt;code&gt;my-app.ts&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you want to learn more about Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once you are happy with how it looks we can move on to Flutter to wrap it in a native app. This will give us access to native code if we wanted to use the in app purchase api or push notifications.&lt;/p&gt;
&lt;p&gt;Kill the terminal and run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter packages get
flutter build ios
flutter build appbundle
flutter run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should select a running device or prompt you to select one. Now that it is running on the device you can see we have two way communication with the Flutter app and the web component.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to find the source code you can check it out &lt;a href=&quot;https://github.com/rodydavis/flutter_hybrid_template&quot;&gt;here&lt;/a&gt; otherwise thanks for reading and let me know if you have any questions!&lt;/p&gt;
</content:encoded></item><item><title>Multi-touch Canvas with Flutter</title><link>https://rodydavis.com/flutter/multi-touch-canvas/</link><guid isPermaLink="true">https://rodydavis.com/flutter/multi-touch-canvas/</guid><description>Flutter package providing a multi-touch canvas implementation with panning, zooming, object selection, and trackpad support, including project setup instructions and a live demo.</description><pubDate>Mon, 20 Jan 2025 03:07:56 GMT</pubDate><content:encoded>&lt;h1&gt;Multi-touch Canvas with Flutter&lt;/h1&gt;
&lt;p&gt;If you ever wanted to create a canvas in &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt; that needs to be panned in any direction and allow zoom then you also probably tried to create a &lt;a href=&quot;https://api.flutter.dev/flutter/gestures/MultiDragGestureRecognizer-class.html&quot;&gt;MultiGestureRecognizer&lt;/a&gt; or under a &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/GestureDetector-class.html&quot;&gt;GestureDetector&lt;/a&gt; added onPanUpdate and onScaleUpdate and received an error because both can not work at the same time. Even if you have to GestureDetectors then you will still find it does not work how you want and one will always win.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/flutter_multi_touch_canvas&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/flutter_multi_touch_canvas/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the canvas rendering logic used in &lt;a href=&quot;https://widget.studio/&quot;&gt;https://widget.studio&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Multi Touch Goal&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Pan the canvas with two or more fingers&lt;/li&gt;
&lt;li&gt;Zoom the canvas with two fingers only (Pinch/Zoom)&lt;/li&gt;
&lt;li&gt;Single finger will interact with canvas object and detect selection&lt;/li&gt;
&lt;li&gt;Bonus trackpad support with similar results&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In order to achieve this we need to use a Listener for the trackpad events and raw touch interactions and &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/RawKeyboardListener-class.html&quot;&gt;RawKeyboardListener&lt;/a&gt; for keyboard shortcuts.&lt;/p&gt;
&lt;h2&gt;Part 1 - Project Setup&lt;/h2&gt;
&lt;p&gt;Open your terminal and type the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir flutter_multi_touch
cd flutter_multi_touch
flutter create .
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last line is optional and if you have VSCode installed. The command will open the directory inside VSCode.&lt;/p&gt;
&lt;h2&gt;Part 2 - Boilerplate&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Remove all comments&lt;/li&gt;
&lt;li&gt;Remove extra empty lines&lt;/li&gt;
&lt;li&gt;Update UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Right now when you run the project you will have this UI.&lt;/p&gt;
&lt;p&gt;Create a new file located at &lt;code&gt;ui/home/screen.dart&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update &lt;code&gt;main.dart&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;ui/home/screen.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      darkTheme: ThemeData.dark().copyWith(
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: HomeScreen(),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will now have a black screen when you run the application.&lt;/p&gt;
&lt;h2&gt;Part 3 - Creating the Controller&lt;/h2&gt;
&lt;p&gt;Now we want to create a class that will act as our controller on the canvas.&lt;br&gt;
Create a new file at &lt;code&gt;src/controllers/canvas.dart&lt;/code&gt; and add the following to start:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:async&apos;;

/// Control the canvas and the objects on it
class CanvasController {
  // Controller for the stream output
  final _controller = StreamController&amp;lt;CanvasController&amp;gt;();
  // Reference to the stream to update the UI
  Stream&amp;lt;CanvasController&amp;gt; get stream =&amp;gt; _controller.stream;
  // Emit a new event to rebuild the UI
  void add([CanvasController val]) =&amp;gt; _controller.add(val ?? this);
  // Stop the stream and finish
  void close() =&amp;gt; _controller.close();
	// Start the stream
  void init() =&amp;gt; add();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the home screen with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;../../src/controllers/canvas.dart&apos;;

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key key}) : super(key: key);

  @override
  _HomeScreenState createState() =&amp;gt; _HomeScreenState();
}

class _HomeScreenState extends State&amp;lt;HomeScreen&amp;gt; {
  final _controller = CanvasController();

  @override
  void initState() {
    _controller.init();
    super.initState();
  }

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder&amp;lt;CanvasController&amp;gt;(
        stream: _controller.stream,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Scaffold(
              appBar: AppBar(),
              body: Center(child: CircularProgressIndicator()),
            );
          }
          final instance = snapshot.data;
          return Scaffold(
            appBar: AppBar(),
            body: Stack(
              children: [
                Positioned(
                  top: 20,
                  left: 20,
                  width: 100,
                  height: 100,
                  child: Container(color: Colors.red),
                )
              ],
            ),
          );
        });
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are just adding the basics to rebuild when the controller changes or the screen is finished. We are using a stateful widget here because we want to dispose of the controller and load it only once. We are also using a stack because thats all we need under the hood. After a quick hot restart you should have the following view.&lt;/p&gt;
&lt;h2&gt;Part 4 - Adding Canvas Objects&lt;/h2&gt;
&lt;p&gt;Now we need to create the class for the objects that will be stored on the canvas. Create a new file at &lt;code&gt;src/classes/canvas_object.dart&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:ui&apos;;

class CanvasObject&amp;lt;T&amp;gt; {
  final double dx;
  final double dy;
  final double width;
  final double height;
  final T child;

  CanvasObject({
    this.dx = 0,
    this.dy = 0,
    this.width = 100,
    this.height = 100,
    this.child,
  });

  CanvasObject&amp;lt;T&amp;gt; copyWith({
    double dx,
    double dy,
    double width,
    double height,
    T child,
  }) {
    return CanvasObject&amp;lt;T&amp;gt;(
      dx: dx ?? this.dx,
      dy: dy ?? this.dy,
      width: width ?? this.width,
      height: height ?? this.height,
      child: child ?? this.child,
    );
  }

  Size get size =&amp;gt; Size(width, height);
  Offset get offset =&amp;gt; Offset(dx, dy);
  Rect get rect =&amp;gt; offset &amp;amp; size;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are using a generic here to not depend on flutter or material in the class. Update the controller with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:async&apos;;

import &apos;package:flutter/material.dart&apos;;

import &apos;../classes/canvas_object.dart&apos;;

/// Control the canvas and the objects on it
class CanvasController {
  /// Controller for the stream output
  final _controller = StreamController&amp;lt;CanvasController&amp;gt;();

  /// Reference to the stream to update the UI
  Stream&amp;lt;CanvasController&amp;gt; get stream =&amp;gt; _controller.stream;

  /// Emit a new event to rebuild the UI
  void add([CanvasController val]) =&amp;gt; _controller.add(val ?? this);

  /// Stop the stream and finish
  void close() =&amp;gt; _controller.close();

  /// Start the stream
  void init() =&amp;gt; add();

  // -- Canvas Objects --

  final List&amp;lt;CanvasObject&amp;lt;Widget&amp;gt;&amp;gt; _objects = [];

  /// Current Objects on the canvas
  List&amp;lt;CanvasObject&amp;lt;Widget&amp;gt;&amp;gt; get objects =&amp;gt; _objects;

  /// Add an object to the canvas
  void addObject(CanvasObject&amp;lt;Widget&amp;gt; value) =&amp;gt; _update(() {
        _objects.add(value);
      });

  /// Add an object to the canvas
  void updateObject(int i, CanvasObject&amp;lt;Widget&amp;gt; value) =&amp;gt; _update(() {
        _objects[i] = value;
      });

  /// Remove an object from the canvas
  void removeObject(int i) =&amp;gt; _update(() {
        _objects.removeAt(i);
      });

  void _update(void Function() action) {
    action();
    add(this);
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are just adding the objects to the canvas and removing them if needed. Update the home screen with the following to use these new objects:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;../../src/classes/canvas_object.dart&apos;;
import &apos;../../src/controllers/canvas.dart&apos;;

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key key}) : super(key: key);

  @override
  _HomeScreenState createState() =&amp;gt; _HomeScreenState();
}

class _HomeScreenState extends State&amp;lt;HomeScreen&amp;gt; {
  final _controller = CanvasController();

  @override
  void initState() {
    _controller.init();
    _dummyData();
    super.initState();
  }

  void _dummyData() {
    _controller.addObject(
      CanvasObject(
        dx: 20,
        dy: 20,
        width: 100,
        height: 100,
        child: Container(color: Colors.red),
      ),
    );
  }

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder&amp;lt;CanvasController&amp;gt;(
        stream: _controller.stream,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Scaffold(
              appBar: AppBar(),
              body: Center(child: CircularProgressIndicator()),
            );
          }
          final instance = snapshot.data;
          return Scaffold(
            appBar: AppBar(),
            body: Stack(
              children: [
                for (final object in instance.objects)
                  Positioned(
                    top: object.dy,
                    left: object.dx,
                    width: object.width,
                    height: object.height,
                    child: object.child,
                  )
              ],
            ),
          );
        });
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The UI is thee same as before but now is dynamic and we have access to the Stack children and position of each child.&lt;/p&gt;
&lt;h2&gt;Part 5 - Capture the Input&lt;/h2&gt;
&lt;p&gt;We need to capture the input of the MultiGestureRecognizer, GestureDetector and RawKeyboardListener. Update the canvas controller with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:async&apos;;

import &apos;package:flutter/material.dart&apos;;

import &apos;../classes/canvas_object.dart&apos;;

/// Control the canvas and the objects on it
class CanvasController {
  /// Controller for the stream output
  final _controller = StreamController&amp;lt;CanvasController&amp;gt;();

  /// Reference to the stream to update the UI
  Stream&amp;lt;CanvasController&amp;gt; get stream =&amp;gt; _controller.stream;

  /// Emit a new event to rebuild the UI
  void add([CanvasController val]) =&amp;gt; _controller.add(val ?? this);

  /// Stop the stream and finish
  void close() {
    _controller.close();
    focusNode.dispose();
  }

  /// Start the stream
  void init() =&amp;gt; add();

  // -- Canvas Objects --

  final List&amp;lt;CanvasObject&amp;lt;Widget&amp;gt;&amp;gt; _objects = [];

  /// Current Objects on the canvas
  List&amp;lt;CanvasObject&amp;lt;Widget&amp;gt;&amp;gt; get objects =&amp;gt; _objects;

  /// Add an object to the canvas
  void addObject(CanvasObject&amp;lt;Widget&amp;gt; value) =&amp;gt; _update(() {
        _objects.add(value);
      });

  /// Add an object to the canvas
  void updateObject(int i, CanvasObject&amp;lt;Widget&amp;gt; value) =&amp;gt; _update(() {
        _objects[i] = value;
      });

  /// Remove an object from the canvas
  void removeObject(int i) =&amp;gt; _update(() {
        _objects.removeAt(i);
      });

  /// Focus node for listening for keyboard shortcuts
  final focusNode = FocusNode();

  /// Raw events from keys pressed
  void rawKeyEvent(BuildContext context, RawKeyEvent key) {}

  /// Called every time a new finger touches the screen
  void addTouch(int pointer, Offset offsetVal, Offset globalVal) {}

  /// Called when any of the fingers update position
  void updateTouch(int pointer, Offset offsetVal, Offset globalVal) {}

  /// Called when a finger is removed from the screen
  void removeTouch(int pointer) {}

  /// Checks if the shift key on the keyboard is pressed
  bool shiftPressed = false;

  /// Scale of the canvas
  double get scale =&amp;gt; _scale;
  double _scale = 1;
  set scale(double value) =&amp;gt; _update(() {
        _scale = value;
      });

  /// Max possible scale
  static const double maxScale = 3.0;
  /// Min possible scale
  static const double minScale = 0.2;
  /// How much to scale the canvas in increments
  static const double scaleAdjust = 0.05;

  /// Current offset of the canvas
  Offset get offset =&amp;gt; _offset;
  Offset _offset = Offset.zero;
  set offset(Offset value) =&amp;gt; _update(() {
        _offset = value;
      });

  void _update(void Function() action) {
    action();
    add(this);
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the home screen with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/gestures.dart&apos;;
import &apos;package:flutter/material.dart&apos;;

import &apos;../../src/classes/canvas_object.dart&apos;;
import &apos;../../src/controllers/canvas.dart&apos;;

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key key}) : super(key: key);

  @override
  _HomeScreenState createState() =&amp;gt; _HomeScreenState();
}

class _HomeScreenState extends State&amp;lt;HomeScreen&amp;gt; {
  final _controller = CanvasController();

  @override
  void initState() {
    _controller.init();
    _dummyData();
    super.initState();
  }

  void _dummyData() {
    _controller.addObject(
      CanvasObject(
        dx: 20,
        dy: 20,
        width: 100,
        height: 100,
        child: Container(color: Colors.red),
      ),
    );
  }

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder&amp;lt;CanvasController&amp;gt;(
        stream: _controller.stream,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Scaffold(
              appBar: AppBar(),
              body: Center(child: CircularProgressIndicator()),
            );
          }
          final instance = snapshot.data;
          return Scaffold(
            appBar: AppBar(),
            body: Listener(
              behavior: HitTestBehavior.opaque,
              onPointerSignal: (details) {
                if (details is PointerScrollEvent) {
                  GestureBinding.instance.pointerSignalResolver
                      .register(details, (event) {
                    if (event is PointerScrollEvent) {
                      if (_controller.shiftPressed) {
                        double zoomDelta = (-event.scrollDelta.dy / 300);
                        _controller.scale = _controller.scale + zoomDelta;
                      } else {
                        _controller.offset =
                            _controller.offset - event.scrollDelta;
                      }
                    }
                  });
                }
              },
              onPointerMove: (details) {
                _controller.updateTouch(
                  details.pointer,
                  details.localPosition,
                  details.position,
                );
              },
              onPointerDown: (details) {
                _controller.addTouch(
                  details.pointer,
                  details.localPosition,
                  details.position,
                );
              },
              onPointerUp: (details) {
                _controller.removeTouch(details.pointer);
              },
              onPointerCancel: (details) {
                _controller.removeTouch(details.pointer);
              },
              child: RawKeyboardListener(
                autofocus: true,
                focusNode: _controller.focusNode,
                onKey: (key) =&amp;gt; _controller.rawKeyEvent(context, key),
                child: Stack(
                  children: [
                    for (final object in instance.objects)
                      Positioned(
                        top: object.dy,
                        left: object.dx,
                        width: object.width,
                        height: object.height,
                        child: object.child,
                      )
                  ],
                ),
              ),
            ),
          );
        });
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All we are doing now is just mapping the inputs of the UI to the actions in the controller. Feel free to look through the comments if you are curious how each one works. Running the application should still just show the red square.&lt;/p&gt;
&lt;h2&gt;Part 5 - Canvas Offset and Scale&lt;/h2&gt;
&lt;p&gt;Now we want to start moving the canvas. Let’s first tackle the offset as scale will take a different approach. Update the home screen with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/gestures.dart&apos;;
import &apos;package:flutter/material.dart&apos;;

import &apos;../../src/classes/canvas_object.dart&apos;;
import &apos;../../src/controllers/canvas.dart&apos;;

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key key}) : super(key: key);

  @override
  _HomeScreenState createState() =&amp;gt; _HomeScreenState();
}

class _HomeScreenState extends State&amp;lt;HomeScreen&amp;gt; {
  final _controller = CanvasController();

  @override
  void initState() {
    _controller.init();
    _dummyData();
    super.initState();
  }

  void _dummyData() {
    _controller.addObject(
      CanvasObject(
        dx: 20,
        dy: 20,
        width: 100,
        height: 100,
        child: Container(color: Colors.red),
      ),
    );
  }

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder&amp;lt;CanvasController&amp;gt;(
        stream: _controller.stream,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Scaffold(
              appBar: AppBar(),
              body: Center(child: CircularProgressIndicator()),
            );
          }
          final instance = snapshot.data;
          return Scaffold(
            appBar: AppBar(),
            body: Listener(
              behavior: HitTestBehavior.opaque,
              onPointerSignal: (details) {
                if (details is PointerScrollEvent) {
                  GestureBinding.instance.pointerSignalResolver
                      .register(details, (event) {
                    if (event is PointerScrollEvent) {
                      if (_controller.shiftPressed) {
                        double zoomDelta = (-event.scrollDelta.dy / 300);
                        _controller.scale = _controller.scale + zoomDelta;
                      } else {
                        _controller.offset =
                            _controller.offset - event.scrollDelta;
                      }
                    }
                  });
                }
              },
              onPointerMove: (details) {
                _controller.updateTouch(
                  details.pointer,
                  details.localPosition,
                  details.position,
                );
              },
              onPointerDown: (details) {
                _controller.addTouch(
                  details.pointer,
                  details.localPosition,
                  details.position,
                );
              },
              onPointerUp: (details) {
                _controller.removeTouch(details.pointer);
              },
              onPointerCancel: (details) {
                _controller.removeTouch(details.pointer);
              },
              child: RawKeyboardListener(
                autofocus: true,
                focusNode: _controller.focusNode,
                onKey: (key) =&amp;gt; _controller.rawKeyEvent(context, key),
                child: SizedBox.expand(
                  child: Stack(
                    children: [
                      for (final object in instance.objects)
                        AnimatedPositioned.fromRect(
                          duration: const Duration(milliseconds: 50),
                          rect: object.rect.adjusted(
                            _controller.offset,
                            _controller.scale,
                          ),
                          child: FittedBox(
                            fit: BoxFit.fill,
                            child: SizedBox.fromSize(
                              size: object.size,
                              child: object.child,
                            ),
                          ),
                        )
                    ],
                  ),
                ),
              ),
            ),
          );
        });
  }
}

extension RectUtils on Rect {
  Rect adjusted(Offset offset, double scale) {
    final left = (this.left + offset.dx) * scale;
    final top = (this.top + offset.dy) * scale;
    final width = this.width * scale;
    final height = this.height * scale;
    return Rect.fromLTWH(left, top, width, height);
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when you use your trackpad to pan with two fingers you will see the red square move. We now need to add finger support too. You may notice the FittedBox and that will come in as soon as we add scaling.&lt;/p&gt;
&lt;p&gt;Now if we move the square off the screen we may need to bring it back. We can add a reset button to the AppBar. Add the following to the canvas controller:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;  static const double _scaleDefault = 1;
  static const Offset _offsetDefault = Offset.zero;

  void reset() {
    scale = _scaleDefault;
    offset = _offsetDefault;
  }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the home screen with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/gestures.dart&apos;;
import &apos;package:flutter/material.dart&apos;;

import &apos;../../src/classes/canvas_object.dart&apos;;
import &apos;../../src/controllers/canvas.dart&apos;;

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key key}) : super(key: key);

  @override
  _HomeScreenState createState() =&amp;gt; _HomeScreenState();
}

class _HomeScreenState extends State&amp;lt;HomeScreen&amp;gt; {
  final _controller = CanvasController();

  @override
  void initState() {
    _controller.init();
    _dummyData();
    super.initState();
  }

  void _dummyData() {
    _controller.addObject(
      CanvasObject(
        dx: 20,
        dy: 20,
        width: 100,
        height: 100,
        child: Container(color: Colors.red),
      ),
    );
  }

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder&amp;lt;CanvasController&amp;gt;(
        stream: _controller.stream,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Scaffold(
              appBar: AppBar(),
              body: Center(child: CircularProgressIndicator()),
            );
          }
          final instance = snapshot.data;
          return Scaffold(
            appBar: AppBar(
              actions: [
                IconButton(
                  tooltip: &apos;Reset the Scale and Offset&apos;,
                  icon: Icon(Icons.restore),
                  onPressed: _controller.reset,
                ),
              ],
            ),
            body: Listener(
              behavior: HitTestBehavior.opaque,
              onPointerSignal: (details) {
                if (details is PointerScrollEvent) {
                  GestureBinding.instance.pointerSignalResolver
                      .register(details, (event) {
                    if (event is PointerScrollEvent) {
                      if (_controller.shiftPressed) {
                        double zoomDelta = (-event.scrollDelta.dy / 300);
                        _controller.scale = _controller.scale + zoomDelta;
                      } else {
                        _controller.offset =
                            _controller.offset - event.scrollDelta;
                      }
                    }
                  });
                }
              },
              onPointerMove: (details) {
                _controller.updateTouch(
                  details.pointer,
                  details.localPosition,
                  details.position,
                );
              },
              onPointerDown: (details) {
                _controller.addTouch(
                  details.pointer,
                  details.localPosition,
                  details.position,
                );
              },
              onPointerUp: (details) {
                _controller.removeTouch(details.pointer);
              },
              onPointerCancel: (details) {
                _controller.removeTouch(details.pointer);
              },
              child: RawKeyboardListener(
                autofocus: true,
                focusNode: _controller.focusNode,
                onKey: (key) =&amp;gt; _controller.rawKeyEvent(context, key),
                child: SizedBox.expand(
                  child: Stack(
                    children: [
                      for (final object in instance.objects)
                        AnimatedPositioned.fromRect(
                          duration: const Duration(milliseconds: 50),
                          rect: object.rect.adjusted(
                            _controller.offset,
                            _controller.scale,
                          ),
                          child: FittedBox(
                            fit: BoxFit.fill,
                            child: SizedBox.fromSize(
                              size: object.size,
                              child: object.child,
                            ),
                          ),
                        )
                    ],
                  ),
                ),
              ),
            ),
          );
        });
  }
}

extension RectUtils on Rect {
  Rect adjusted(Offset offset, double scale) {
    final left = (this.left + offset.dx) * scale;
    final top = (this.top + offset.dy) * scale;
    final width = this.width * scale;
    final height = this.height * scale;
    return Rect.fromLTWH(left, top, width, height);
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when you press the reset button the canvas animates back to the default offset and scale.&lt;/p&gt;
&lt;p&gt;While we are here we can add actions for zoom in/out and connect them to the controller. Add the following to the canvas controller:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;  void zoomIn() {
    scale += scaleAdjust;
  }

  void zoomOut() {
    scale -= scaleAdjust;
  }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the following to the AppBar actions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;IconButton(
    tooltip: &apos;Zoom In&apos;,
    icon: Icon(Icons.zoom_in),
    onPressed: _controller.zoomIn,
 ),
IconButton(
    tooltip: &apos;Zoom Out&apos;,
    icon: Icon(Icons.zoom_out),
    onPressed: _controller.zoomOut,
),

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when you run the application you can easily zoom in/out.&lt;/p&gt;
&lt;h2&gt;Part 6 - Keyboard Shortcuts&lt;/h2&gt;
&lt;p&gt;Now we need to capture the keyboard events so we can move the canvas with the arrow keys and scale with +/- keys. Update the controller with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:async&apos;;

import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter/services.dart&apos;;

import &apos;../classes/canvas_object.dart&apos;;

/// Control the canvas and the objects on it
class CanvasController {
  /// Controller for the stream output
  final _controller = StreamController&amp;lt;CanvasController&amp;gt;();

  /// Reference to the stream to update the UI
  Stream&amp;lt;CanvasController&amp;gt; get stream =&amp;gt; _controller.stream;

  /// Emit a new event to rebuild the UI
  void add([CanvasController val]) =&amp;gt; _controller.add(val ?? this);

  /// Stop the stream and finish
  void close() {
    _controller.close();
    focusNode.dispose();
  }

  /// Start the stream
  void init() =&amp;gt; add();

  // -- Canvas Objects --

  final List&amp;lt;CanvasObject&amp;lt;Widget&amp;gt;&amp;gt; _objects = [];

  /// Current Objects on the canvas
  List&amp;lt;CanvasObject&amp;lt;Widget&amp;gt;&amp;gt; get objects =&amp;gt; _objects;

  /// Add an object to the canvas
  void addObject(CanvasObject&amp;lt;Widget&amp;gt; value) =&amp;gt; _update(() {
        _objects.add(value);
      });

  /// Add an object to the canvas
  void updateObject(int i, CanvasObject&amp;lt;Widget&amp;gt; value) =&amp;gt; _update(() {
        _objects[i] = value;
      });

  /// Remove an object from the canvas
  void removeObject(int i) =&amp;gt; _update(() {
        _objects.removeAt(i);
      });

  /// Focus node for listening for keyboard shortcuts
  final focusNode = FocusNode();

  /// Raw events from keys pressed
  void rawKeyEvent(BuildContext context, RawKeyEvent key) {
    // Scale keys
    if (key.isKeyPressed(LogicalKeyboardKey.minus)) {
      zoomOut();
    }
    if (key.isKeyPressed(LogicalKeyboardKey.equal)) {
      zoomIn();
    }
    // Directional Keys
    if (key.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {
      offset = offset + Offset(offsetAdjust, 0.0);
    }
    if (key.isKeyPressed(LogicalKeyboardKey.arrowRight)) {
      offset = offset + Offset(-offsetAdjust, 0.0);
    }
    if (key.isKeyPressed(LogicalKeyboardKey.arrowUp)) {
      offset = offset + Offset(0.0, offsetAdjust);
    }
    if (key.isKeyPressed(LogicalKeyboardKey.arrowDown)) {
      offset = offset + Offset(0.0, -offsetAdjust);
    }

    _shiftPressed = key.isShiftPressed;

    /// Update Controller Instance
    add(this);
  }

  /// Called every time a new finger touches the screen
  void addTouch(int pointer, Offset offsetVal, Offset globalVal) {}

  /// Called when any of the fingers update position
  void updateTouch(int pointer, Offset offsetVal, Offset globalVal) {}

  /// Called when a finger is removed from the screen
  void removeTouch(int pointer) {}

  /// Checks if the shift key on the keyboard is pressed
  bool get shiftPressed =&amp;gt; _shiftPressed;
  bool _shiftPressed = false;

  /// Scale of the canvas
  double get scale =&amp;gt; _scale;
  double _scale = 1;
  set scale(double value) =&amp;gt; _update(() {
        _scale = value;
      });

  /// Max possible scale
  static const double maxScale = 3.0;

  /// Min possible scale
  static const double minScale = 0.2;

  /// How much to scale the canvas in increments
  static const double scaleAdjust = 0.05;

  /// How much to shift the canvas in increments
  static const double offsetAdjust = 15;

  /// Current offset of the canvas
  Offset get offset =&amp;gt; _offset;
  Offset _offset = Offset.zero;
  set offset(Offset value) =&amp;gt; _update(() {
        _offset = value;
      });

  static const double _scaleDefault = 1;
  static const Offset _offsetDefault = Offset.zero;

  /// Reset the canvas zoom and offset
  void reset() {
    scale = _scaleDefault;
    offset = _offsetDefault;
  }

  /// Zoom in the canvas
  void zoomIn() {
    scale += scaleAdjust;
  }

  /// Zoom out the canvas
  void zoomOut() {
    scale -= scaleAdjust;
  }

  void _update(void Function() action) {
    action();
    add(this);
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when you run the application you can control the zoom and pan with just a keyboard. This could be useful for a fallback input that would work on a TV for example…&lt;/p&gt;
&lt;p&gt;If you want to see if it is actually scaling proportionally then add the following the home screen:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/gestures.dart&apos;;
import &apos;package:flutter/material.dart&apos;;

import &apos;../../src/classes/canvas_object.dart&apos;;
import &apos;../../src/controllers/canvas.dart&apos;;

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key key}) : super(key: key);

  @override
  _HomeScreenState createState() =&amp;gt; _HomeScreenState();
}

class _HomeScreenState extends State&amp;lt;HomeScreen&amp;gt; {
  final _controller = CanvasController();

  @override
  void initState() {
    _controller.init();
    _dummyData();
    super.initState();
  }

  void _dummyData() {
    _controller.addObject(
      CanvasObject(
        dx: 20,
        dy: 20,
        width: 100,
        height: 100,
        child: Container(color: Colors.red),
      ),
    );
    _controller.addObject(
      CanvasObject(
        dx: 80,
        dy: 60,
        width: 100,
        height: 200,
        child: Container(color: Colors.green),
      ),
    );
    _controller.addObject(
      CanvasObject(
        dx: 100,
        dy: 40,
        width: 100,
        height: 50,
        child: Container(color: Colors.blue),
      ),
    );
  }

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder&amp;lt;CanvasController&amp;gt;(
        stream: _controller.stream,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Scaffold(
              appBar: AppBar(),
              body: Center(child: CircularProgressIndicator()),
            );
          }
          final instance = snapshot.data;
          return Scaffold(
            appBar: AppBar(
              actions: [
                FocusScope(
                  canRequestFocus: false,
                  child: IconButton(
                    tooltip: &apos;Zoom In&apos;,
                    icon: Icon(Icons.zoom_in),
                    onPressed: _controller.zoomIn,
                  ),
                ),
                FocusScope(
                  canRequestFocus: false,
                  child: IconButton(
                    tooltip: &apos;Zoom Out&apos;,
                    icon: Icon(Icons.zoom_out),
                    onPressed: _controller.zoomOut,
                  ),
                ),
                FocusScope(
                  canRequestFocus: false,
                  child: IconButton(
                    tooltip: &apos;Reset the Scale and Offset&apos;,
                    icon: Icon(Icons.restore),
                    onPressed: _controller.reset,
                  ),
                ),
              ],
            ),
            body: Listener(
              behavior: HitTestBehavior.opaque,
              onPointerSignal: (details) {
                if (details is PointerScrollEvent) {
                  GestureBinding.instance.pointerSignalResolver
                      .register(details, (event) {
                    if (event is PointerScrollEvent) {
                      if (_controller.shiftPressed) {
                        double zoomDelta = (-event.scrollDelta.dy / 300);
                        _controller.scale = _controller.scale + zoomDelta;
                      } else {
                        _controller.offset =
                            _controller.offset - event.scrollDelta;
                      }
                    }
                  });
                }
              },
              onPointerMove: (details) {
                _controller.updateTouch(
                  details.pointer,
                  details.localPosition,
                  details.position,
                );
              },
              onPointerDown: (details) {
                _controller.addTouch(
                  details.pointer,
                  details.localPosition,
                  details.position,
                );
              },
              onPointerUp: (details) {
                _controller.removeTouch(details.pointer);
              },
              onPointerCancel: (details) {
                _controller.removeTouch(details.pointer);
              },
              child: RawKeyboardListener(
                autofocus: true,
                focusNode: _controller.focusNode,
                onKey: (key) =&amp;gt; _controller.rawKeyEvent(context, key),
                child: SizedBox.expand(
                  child: Stack(
                    children: [
                      for (final object in instance.objects)
                        AnimatedPositioned.fromRect(
                          duration: const Duration(milliseconds: 50),
                          rect: object.rect.adjusted(
                            _controller.offset,
                            _controller.scale,
                          ),
                          child: FittedBox(
                            fit: BoxFit.fill,
                            child: SizedBox.fromSize(
                              size: object.size,
                              child: object.child,
                            ),
                          ),
                        )
                    ],
                  ),
                ),
              ),
            ),
          );
        });
  }
}

extension RectUtils on Rect {
  Rect adjusted(Offset offset, double scale) {
    final left = (this.left + offset.dx) * scale;
    final top = (this.top + offset.dy) * scale;
    final width = this.width * scale;
    final height = this.height * scale;
    return Rect.fromLTWH(left, top, width, height);
  }
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can zoom and the blocks all scale correctly and pan around.&lt;/p&gt;
&lt;p&gt;Just press the reset button to start over.&lt;/p&gt;
&lt;h2&gt;Part 7 - Multi Touch Input&lt;/h2&gt;
&lt;p&gt;Now time for the fingers. For this you will need a touchscreen device to test. You can plug in your phone or if you have a touch screen computer you can run the web version. Update the controller with following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:async&apos;;
import &apos;dart:math&apos; as math;

import &apos;package:flutter/gestures.dart&apos;;
import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter/services.dart&apos;;

import &apos;../classes/canvas_object.dart&apos;;
import &apos;../classes/rect_points.dart&apos;;

/// Control the canvas and the objects on it
class CanvasController {
  /// Controller for the stream output
  final _controller = StreamController&amp;lt;CanvasController&amp;gt;();

  /// Reference to the stream to update the UI
  Stream&amp;lt;CanvasController&amp;gt; get stream =&amp;gt; _controller.stream;

  /// Emit a new event to rebuild the UI
  void add([CanvasController val]) =&amp;gt; _controller.add(val ?? this);

  /// Stop the stream and finish
  void close() {
    _controller.close();
    focusNode.dispose();
  }

  /// Start the stream
  void init() =&amp;gt; add();

  // -- Canvas Objects --

  final List&amp;lt;CanvasObject&amp;lt;Widget&amp;gt;&amp;gt; _objects = [];

  /// Current Objects on the canvas
  List&amp;lt;CanvasObject&amp;lt;Widget&amp;gt;&amp;gt; get objects =&amp;gt; _objects;

  /// Add an object to the canvas
  void addObject(CanvasObject&amp;lt;Widget&amp;gt; value) =&amp;gt; _update(() {
        _objects.add(value);
      });

  /// Add an object to the canvas
  void updateObject(int i, CanvasObject&amp;lt;Widget&amp;gt; value) =&amp;gt; _update(() {
        _objects[i] = value;
      });

  /// Remove an object from the canvas
  void removeObject(int i) =&amp;gt; _update(() {
        _objects.removeAt(i);
      });

  /// Focus node for listening for keyboard shortcuts
  final focusNode = FocusNode();

  /// Raw events from keys pressed
  void rawKeyEvent(BuildContext context, RawKeyEvent key) {
    // Scale keys
    if (key.isKeyPressed(LogicalKeyboardKey.minus)) {
      zoomOut();
    }
    if (key.isKeyPressed(LogicalKeyboardKey.equal)) {
      zoomIn();
    }
    // Directional Keys
    if (key.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {
      offset = offset + Offset(offsetAdjust, 0.0);
    }
    if (key.isKeyPressed(LogicalKeyboardKey.arrowRight)) {
      offset = offset + Offset(-offsetAdjust, 0.0);
    }
    if (key.isKeyPressed(LogicalKeyboardKey.arrowUp)) {
      offset = offset + Offset(0.0, offsetAdjust);
    }
    if (key.isKeyPressed(LogicalKeyboardKey.arrowDown)) {
      offset = offset + Offset(0.0, -offsetAdjust);
    }

    _shiftPressed = key.isShiftPressed;
    _metaPressed = key.isMetaPressed;

    /// Update Controller Instance
    add(this);
  }

  /// Trigger Shift Press
  void shiftSelect() {
    _shiftPressed = true;
  }

  /// Trigger Meta Press
  void metaSelect() {
    _metaPressed = true;
  }

  final Map&amp;lt;int, Offset&amp;gt; _pointerMap = {};

  /// Number of inputs currently on the screen
  int get touchCount =&amp;gt; _pointerMap.values.length;

  /// Marquee selection on the canvas
  RectPoints get marquee =&amp;gt; _marquee;
  RectPoints _marquee;

  /// Dragging a canvas object
  bool get isMovingCanvasObject =&amp;gt; _isMovingCanvasObject;
  bool _isMovingCanvasObject = false;

  final List&amp;lt;int&amp;gt; _selectedObjects = [];
  List&amp;lt;int&amp;gt; get selectedObjectsIndices =&amp;gt; _selectedObjects;
  List&amp;lt;CanvasObject&amp;lt;Widget&amp;gt;&amp;gt; get selectedObjects =&amp;gt;
      _selectedObjects.map((i) =&amp;gt; _objects[i]).toList();
  bool isObjectSelected(int i) =&amp;gt; _selectedObjects.contains(i);

  /// Called every time a new input touches the screen
  void addTouch(int pointer, Offset offsetVal, Offset globalVal) {
    _pointerMap[pointer] = offsetVal;

    if (shiftPressed) {
      final pt = (offsetVal / scale) - (offset);
      _marquee = RectPoints(pt, pt);
    }

    /// Update Controller Instance
    add(this);
  }

  /// Called when any of the inputs update position
  void updateTouch(int pointer, Offset offsetVal, Offset globalVal) {
    if (_marquee != null) {
      // Update New Widget Rect
      final _pts = _marquee;
      final a = _pointerMap.values.first;
      _pointerMap[pointer] = offsetVal;
      final b = _pointerMap.values.first;
      final delta = (b - a) / scale;
      _pts.end = _pts.end + delta;
      _marquee = _pts;
      final _rect = Rect.fromPoints(_pts.start, _pts.end);
      _selectedObjects.clear();
      for (var i = 0; i &amp;lt; _objects.length; i++) {
        if (_rect.overlaps(_objects[i].rect)) {
          _selectedObjects.add(i);
        }
      }
    } else if (touchCount == 1) {
      // Widget Move
      _isMovingCanvasObject = true;
      final a = _pointerMap.values.first;
      _pointerMap[pointer] = offsetVal;
      final b = _pointerMap.values.first;
      if (_selectedObjects.isEmpty) return;
      for (final idx in _selectedObjects) {
        final widget = _objects[idx];
        final delta = (b - a) / scale;
        final _newOffset = widget.offset + delta;
        _objects[idx] = widget.copyWith(dx: _newOffset.dx, dy: _newOffset.dy);
      }
    } else if (touchCount == 2) {
      // Scale and Rotate Update
      _isMovingCanvasObject = false;
      final _rectA = _getRectFromPoints(_pointerMap.values.toList());
      _pointerMap[pointer] = offsetVal;
      final _rectB = _getRectFromPoints(_pointerMap.values.toList());
      final _delta = _rectB.center - _rectA.center;
      final _newOffset = offset + (_delta / scale);
      offset = _newOffset;
      final aDistance = (_rectA.topLeft - _rectA.bottomRight).distance;
      final bDistance = (_rectB.topLeft - _rectB.bottomRight).distance;
      final change = (bDistance / aDistance);
      scale = scale * change;
    } else {
      // Pan Update
      _isMovingCanvasObject = false;
      final _rectA = _getRectFromPoints(_pointerMap.values.toList());
      _pointerMap[pointer] = offsetVal;
      final _rectB = _getRectFromPoints(_pointerMap.values.toList());
      final _delta = _rectB.center - _rectA.center;
      offset = offset + (_delta / scale);
    }
    _pointerMap[pointer] = offsetVal;

    /// Update Controller Instance
    add(this);
  }

  /// Called when a input is removed from the screen
  void removeTouch(int pointer) {
    _pointerMap.remove(pointer);

    if (touchCount &amp;lt; 1) {
      _isMovingCanvasObject = false;
    }
    if (_marquee != null) {
      _marquee = null;
      _shiftPressed = false;
    }

    /// Update Controller Instance
    add(this);
  }

  void selectObject(int i) =&amp;gt; _update(() {
        if (!_metaPressed) {
          _selectedObjects.clear();
        }
        _selectedObjects.add(0);
        final item = _objects.removeAt(i);
        _objects.insert(0, item);
      });

  /// Checks if the shift key on the keyboard is pressed
  bool get shiftPressed =&amp;gt; _shiftPressed;
  bool _shiftPressed = false;

  /// Checks if the meta key on the keyboard is pressed
  bool get metaPressed =&amp;gt; _metaPressed;
  bool _metaPressed = false;

  /// Scale of the canvas
  double get scale =&amp;gt; _scale;
  double _scale = 1;
  set scale(double value) =&amp;gt; _update(() {
        if (value &amp;lt;= minScale) {
          value = minScale;
        } else if (value &amp;gt;= maxScale) {
          value = maxScale;
        }
        _scale = value;
      });

  /// Max possible scale
  static const double maxScale = 3.0;

  /// Min possible scale
  static const double minScale = 0.2;

  /// How much to scale the canvas in increments
  static const double scaleAdjust = 0.05;

  /// How much to shift the canvas in increments
  static const double offsetAdjust = 15;

  /// Current offset of the canvas
  Offset get offset =&amp;gt; _offset;
  Offset _offset = Offset.zero;
  set offset(Offset value) =&amp;gt; _update(() {
        _offset = value;
      });

  static const double _scaleDefault = 1;
  static const Offset _offsetDefault = Offset.zero;

  /// Reset the canvas zoom and offset
  void reset() {
    scale = _scaleDefault;
    offset = _offsetDefault;
  }

  /// Zoom in the canvas
  void zoomIn() {
    scale += scaleAdjust;
  }

  /// Zoom out the canvas
  void zoomOut() {
    scale -= scaleAdjust;
  }

  void _update(void Function() action) {
    action();
    add(this);
  }

  Rect _getRectFromPoints(List&amp;lt;Offset&amp;gt; offsets) {
    if (offsets.length == 2) {
      return Rect.fromPoints(offsets.first, offsets.last);
    }
    final dxs = offsets.map((e) =&amp;gt; e.dx).toList();
    final dys = offsets.map((e) =&amp;gt; e.dy).toList();
    double left = _minFromList(dxs);
    double top = _minFromList(dys);
    double bottom = _maxFromList(dys);
    double right = _maxFromList(dxs);
    return Rect.fromLTRB(left, top, right, bottom);
  }

  double _minFromList(List&amp;lt;double&amp;gt; values) {
    double value = double.infinity;
    for (final item in values) {
      value = math.min(item, value);
    }
    return value;
  }

  double _maxFromList(List&amp;lt;double&amp;gt; values) {
    double value = -double.infinity;
    for (final item in values) {
      value = math.max(item, value);
    }
    return value;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add a new file &lt;code&gt;src/classes/rect_points.dart&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:ui&apos;;

class RectPoints {
  RectPoints(this.start, this.end);

  Offset start, end;

  Rect get rect =&amp;gt; Rect.fromPoints(start, end);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;main.dart&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;ui/home/screen.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        accentColor: Colors.red,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      darkTheme: ThemeData.dark().copyWith(
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: HomeScreen(),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the home screen with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/gestures.dart&apos;;
import &apos;package:flutter/material.dart&apos;;

import &apos;../../src/classes/canvas_object.dart&apos;;
import &apos;../../src/controllers/canvas.dart&apos;;

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key key}) : super(key: key);

  @override
  _HomeScreenState createState() =&amp;gt; _HomeScreenState();
}

class _HomeScreenState extends State&amp;lt;HomeScreen&amp;gt; {
  final _controller = CanvasController();

  @override
  void initState() {
    _controller.init();
    _dummyData();
    super.initState();
  }

  void _dummyData() {
    _controller.addObject(
      CanvasObject(
        dx: 20,
        dy: 20,
        width: 100,
        height: 100,
        child: Container(color: Colors.red),
      ),
    );
    _controller.addObject(
      CanvasObject(
        dx: 80,
        dy: 60,
        width: 100,
        height: 200,
        child: Container(color: Colors.green),
      ),
    );
    _controller.addObject(
      CanvasObject(
        dx: 100,
        dy: 40,
        width: 100,
        height: 50,
        child: Container(color: Colors.blue),
      ),
    );
  }

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder&amp;lt;CanvasController&amp;gt;(
        stream: _controller.stream,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Scaffold(
              appBar: AppBar(),
              body: Center(child: CircularProgressIndicator()),
            );
          }
          final instance = snapshot.data;
          return Scaffold(
            appBar: AppBar(
              actions: [
                FocusScope(
                  canRequestFocus: false,
                  child: IconButton(
                    tooltip: &apos;Selection&apos;,
                    icon: Icon(Icons.select_all),
                    color: instance.shiftPressed
                        ? Theme.of(context).accentColor
                        : null,
                    onPressed: _controller.shiftSelect,
                  ),
                ),
                FocusScope(
                  canRequestFocus: false,
                  child: IconButton(
                    tooltip: &apos;Meta Key&apos;,
                    color: instance.metaPressed
                        ? Theme.of(context).accentColor
                        : null,
                    icon: Icon(Icons.category),
                    onPressed: _controller.metaSelect,
                  ),
                ),
                FocusScope(
                  canRequestFocus: false,
                  child: IconButton(
                    tooltip: &apos;Zoom In&apos;,
                    icon: Icon(Icons.zoom_in),
                    onPressed: _controller.zoomIn,
                  ),
                ),
                FocusScope(
                  canRequestFocus: false,
                  child: IconButton(
                    tooltip: &apos;Zoom Out&apos;,
                    icon: Icon(Icons.zoom_out),
                    onPressed: _controller.zoomOut,
                  ),
                ),
                FocusScope(
                  canRequestFocus: false,
                  child: IconButton(
                    tooltip: &apos;Reset the Scale and Offset&apos;,
                    icon: Icon(Icons.restore),
                    onPressed: _controller.reset,
                  ),
                ),
              ],
            ),
            body: Listener(
              behavior: HitTestBehavior.opaque,
              onPointerSignal: (details) {
                if (details is PointerScrollEvent) {
                  GestureBinding.instance.pointerSignalResolver
                      .register(details, (event) {
                    if (event is PointerScrollEvent) {
                      if (_controller.shiftPressed) {
                        double zoomDelta = (-event.scrollDelta.dy / 300);
                        _controller.scale = _controller.scale + zoomDelta;
                      } else {
                        _controller.offset =
                            _controller.offset - event.scrollDelta;
                      }
                    }
                  });
                }
              },
              onPointerMove: (details) {
                _controller.updateTouch(
                  details.pointer,
                  details.localPosition,
                  details.position,
                );
              },
              onPointerDown: (details) {
                _controller.addTouch(
                  details.pointer,
                  details.localPosition,
                  details.position,
                );
              },
              onPointerUp: (details) {
                _controller.removeTouch(details.pointer);
              },
              onPointerCancel: (details) {
                _controller.removeTouch(details.pointer);
              },
              child: RawKeyboardListener(
                autofocus: true,
                focusNode: _controller.focusNode,
                onKey: (key) =&amp;gt; _controller.rawKeyEvent(context, key),
                child: SizedBox.expand(
                  child: Stack(
                    children: [
                      for (var i = 0; i &amp;lt; instance.objects.length; i++)
                        Positioned.fromRect(
                          rect: instance.objects[i].rect.adjusted(
                            _controller.offset,
                            _controller.scale,
                          ),
                          child: Container(
                            decoration: BoxDecoration(
                                border: Border.all(
                              color: instance.isObjectSelected(i)
                                  ? Colors.grey
                                  : Colors.transparent,
                            )),
                            child: GestureDetector(
                              onTapDown: (_) =&amp;gt; _controller.selectObject(i),
                              child: FittedBox(
                                fit: BoxFit.fill,
                                child: SizedBox.fromSize(
                                  size: instance.objects[i].size,
                                  child: instance.objects[i].child,
                                ),
                              ),
                            ),
                          ),
                        ),
                      if (instance?.marquee != null)
                        Positioned.fromRect(
                          rect: instance.marquee.rect
                              .adjusted(instance.offset, instance.scale),
                          child: Container(
                            color: Colors.blueAccent.withOpacity(0.3),
                          ),
                        ),
                    ],
                  ),
                ),
              ),
            ),
          );
        });
  }
}

extension RectUtils on Rect {
  Rect adjusted(Offset offset, double scale) {
    final left = (this.left + offset.dx) * scale;
    final top = (this.top + offset.dy) * scale;
    final width = this.width * scale;
    final height = this.height * scale;
    return Rect.fromLTWH(left, top, width, height);
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can move any object on the canvas just by clicking and dragging. You can zoom with 2 fingers and pan with 2 or 3 fingers. If you hold down the shift key then you can use a marquee to select multiple and if you hold down the meta/command key then you can select multiple by tapping each.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you are on a device without a keyboard you can tap the new icons to turn on the keyboard key actions. When the object is selected there is a grey border.&lt;/p&gt;
&lt;p&gt;Now you can add any widget to the canvas and pan and zoom!&lt;/p&gt;
</content:encoded></item><item><title>How to build a native cross platform project with Flutter</title><link>https://rodydavis.com/flutter/native-cross-platform/</link><guid isPermaLink="true">https://rodydavis.com/flutter/native-cross-platform/</guid><description>Learn how to create native cross-platform Flutter projects with web support, enabling the use of all plugins and a streamlined development experience.</description><pubDate>Mon, 20 Jan 2025 00:56:31 GMT</pubDate><content:encoded>&lt;h1&gt;How to build a native cross platform project with Flutter&lt;/h1&gt;
&lt;p&gt;Import dart:html and dart:io in the same project!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/flutter_x/tree/finish&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Up to now you have been able to create projects with Flutter that run on iOS/Android, Web and Desktop but only sharing pure dart plugins.&lt;/p&gt;
&lt;p&gt;Flutter launched &lt;em&gt;Flutter for web&lt;/em&gt; at Google I/O and was a temporary fork that required you to change imports from &lt;code&gt;import &apos;package:flutter/material.dart&apos;;&lt;/code&gt; to &lt;code&gt;import &apos;package:flutter_web/material.dart&apos;;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As you can image this was really difficult for a code base as you had to create a fork and change the imports. This also meant that you could not import any package that needed on a path or depended on flutter. The time as come and the merge is complete. Now you no longer need to change the imports!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/n_2_kvxta7kwbn.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can use any plugin now, have a debugger, create new flutter projects with the web folder added, web plugins, and so much more..&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;You will need to be on the latest flutter for this to work.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://flutter.io/get-started/install/&quot;&gt;Download Flutter&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/ff_1_u67ip3pbk5.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you are pretty new to Flutter you can check out &lt;a href=&quot;https://flutter.io/get-started/codelab/&quot;&gt;this useful guide&lt;/a&gt; on how to create a new project step by step.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/ff_2_9dljl4hgqq.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Create a new project named &lt;strong&gt;flutter_x&lt;/strong&gt; and it should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_1_mdevnjnwh7.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can also down the starter project &lt;a href=&quot;https://github.com/rodydavis/flutter_x/tree/starter&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Your code should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with &amp;quot;flutter run&amp;quot;. You&apos;ll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // &amp;quot;hot reload&amp;quot; (press &amp;quot;r&amp;quot; in the console where you ran &amp;quot;flutter run&amp;quot;,
        // or simply save your changes to &amp;quot;hot reload&amp;quot; in a Flutter IDE).
        // Notice that the counter didn&apos;t reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: &apos;Flutter Demo Home Page&apos;),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked &amp;quot;final&amp;quot;.

  final String title;

  @override
  _MyHomePageState createState() =&amp;gt; _MyHomePageState();
}

class _MyHomePageState extends State&amp;lt;MyHomePage&amp;gt; {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke &amp;quot;debug painting&amp;quot; (press &amp;quot;p&amp;quot; in the console, choose the
          // &amp;quot;Toggle Debug Paint&amp;quot; action from the Flutter Inspector in Android
          // Studio, or the &amp;quot;Toggle Debug Paint&amp;quot; command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: &amp;lt;Widget&amp;gt;[
            Text(
              &apos;You have pushed the button this many times:&apos;,
            ),
            Text(
              &apos;$_counter&apos;,
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: &apos;Increment&apos;,
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just to make sure everything is working go ahead and run the project on iOS/Android.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_2_srn7elmk0c.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You should have the counter application running and working correctly. Now quit and run on Chrome. It should be listed as a device. You can also run from the command line flutter run -d chrome.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_3_iq2vozt6xi.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You do not get hot reload yet on web so be aware of that.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_4_f6ht1u443m.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Your project should now look like this.&lt;/p&gt;
&lt;p&gt;Open your pubspec.yaml and import the following packages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;dependencies:
  universal_html:
  url_launcher:
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also remove the comments generated in the pubspec.yaml&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Your pubspec.yaml will now read like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;name: flutter_x
description: A new Flutter project.

version: 1.0.0+1

environment:
  sdk: &amp;quot;&amp;gt;=2.1.0 &amp;lt;3.0.0&amp;quot;

dependencies:
  flutter:
    sdk: flutter

cupertino_icons: ^0.1.2
  universal_html: ^1.1.0
  url_launcher: ^5.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:

uses-material-design: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default if you were to check if the device was mobile or web you will get an error at compile time when trying to import a plugin that is not meant for the platform. To get around this we will use dynamic imports.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_5_a7g3ejp2xp.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Create a url_launcher folder and file url_launcher.dart, mobile.dart, web.dart, unsupported.dart inside the plugins folder.&lt;/p&gt;
&lt;p&gt;In the file url_launcher.dart add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;export &apos;unsupported.dart&apos;
    if (dart.library.html) &apos;web.dart&apos;
    if (dart.library.io) &apos;mobile.dart&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will pick the correct file at runtime and give a fallback if it is not supported.&lt;/p&gt;
&lt;p&gt;To protect against edge cases you will need to set up a fallback for the import. In unsupported.dart add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class UrlUtils {
  UrlUtils._();

static void open(String url, {String name}) {
    throw &apos;Platform Not Supported&apos;;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The class UrlUtils and the public methods have to match all three files for this to work correctly. Always set up the unsupported first then copy the file into mobile.dart and web.dart to ensure no typos.&lt;/p&gt;
&lt;p&gt;You should now have 3 files with the above code in each class.&lt;/p&gt;
&lt;p&gt;In mobile.dart add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:url_launcher/url_launcher.dart&apos;;

class UrlUtils {
  UrlUtils._();

static void open(String url, {String name}) async {
    if (await canLaunch(url)) {
      await launch(url);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will open the link in safari view controller or android’s default browser respectively.&lt;/p&gt;
&lt;p&gt;In web.dart add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:universal_html/prefer_universal/html.dart&apos; as html;

class UrlUtils {
  UrlUtils._();

static void open(String url, {String name}) {
    html.window.open(url, name);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will open up a new window in the browser with the specified link.&lt;/p&gt;
&lt;p&gt;Add a button to the center of the screen. The ui/home/screen.dart should read the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
  _MyHomePageState createState() =&amp;gt; _MyHomePageState();
}

class _MyHomePageState extends State&amp;lt;MyHomePage&amp;gt; {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          child: RaisedButton(
        child: Text(&apos;Open Flutter.dev&apos;),
        onPressed: () {},
      )),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the onPressed to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;onPressed: () {
    try {
        UrlUtils.open(&apos;[https://flutter.dev&apos;](https://flutter.dev&apos;));
    } catch (e) {
        print(&apos;Error -&amp;gt; $e&apos;);
    }
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when you go to import the UrlUtils it is important to import the correct URI.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_6_z13qycty3g.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Make sure to import &lt;code&gt;import &apos;package:flutter_x/plugins/url_launcher/url_launcher.dart&apos;;&lt;/code&gt; only.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can use the relative import if you wish.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You UI code will now read the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;../../plugins/url_launcher/url_launcher.dart&apos;;

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
  _MyHomePageState createState() =&amp;gt; _MyHomePageState();
}

class _MyHomePageState extends State&amp;lt;MyHomePage&amp;gt; {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          child: RaisedButton(
        child: Text(&apos;Open Flutter.dev&apos;),
        onPressed: () {
          try {
            UrlUtils.open(&apos;[https://flutter.dev&apos;](https://flutter.dev&apos;));
          } catch (e) {
            print(&apos;Error -&amp;gt; $e&apos;);
          }
        },
      )),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your app on the &lt;strong&gt;web&lt;/strong&gt; should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_7_gr9gvvqov4.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;And when you tap the button..&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_8_kw9mhvznx0.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;And when you run it on &lt;strong&gt;iOS&lt;/strong&gt;/&lt;strong&gt;Android&lt;/strong&gt; it should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_9_af3wexxmw2.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;And when you tap the button..&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_10_c184w6lu7g.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Congratulations! You made it 🎉&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/xx_11_k1m9m1qs1w.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here is the final project located &lt;a href=&quot;https://github.com/rodydavis/flutter_x/tree/finish&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Please reach out if you have any questions!&lt;/p&gt;
</content:encoded></item><item><title>Signals and Flutter Hooks</title><link>https://rodydavis.com/flutter/signals-and-hooks/</link><guid isPermaLink="true">https://rodydavis.com/flutter/signals-and-hooks/</guid><description>Flutter&apos;s `setState` method manages widget state, but using `ValueNotifier` in a container provides a more efficient and manageable way to update the UI based on data changes, especially with widgets like `ValueListenableBuilder`.</description><pubDate>Fri, 31 Jan 2025 12:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Signals and Flutter Hooks&lt;/h1&gt;
&lt;p&gt;When working with data in &lt;a href=&quot;https://flutter.dev&quot;&gt;Flutter&lt;/a&gt;, on of the first things you are exposed to is &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/State/setState.html&quot;&gt;setState&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;setState&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State&amp;lt;Counter&amp;gt; createState() =&amp;gt; _CounterState();
}

class _CounterState extends State&amp;lt;Counter&amp;gt; {
  int count = 0;

  void increment() {
    if (mounted) {
      setState(() {
        count++;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text(&apos;Counter&apos;)),
      body: Center(child: Text(&apos;Count: $count&apos;)),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This simply marks the widget as dirty every time you call &lt;strong&gt;setState&lt;/strong&gt; but requires you (as the developer) to be mindful and explict about when those updates happen. If you forget to call &lt;strong&gt;setState&lt;/strong&gt; when mutating data the widget tree can become stale.&lt;/p&gt;
&lt;h2&gt;ValueNotifier&lt;/h2&gt;
&lt;p&gt;We can impove this by using &lt;a href=&quot;https://api.flutter.dev/flutter/foundation/ValueNotifier-class.html&quot;&gt;ValueNotifier&lt;/a&gt; instead of storing the value directly. This gives us the ability to read and write a value in a container and use helper widgets like &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/ValueListenableBuilder-class.html&quot;&gt;ValueListenableBuilder&lt;/a&gt; to update sub parts of the widget tree on value changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State&amp;lt;Counter&amp;gt; createState() =&amp;gt; _CounterState();
}

class _CounterState extends State&amp;lt;Counter&amp;gt; {
  final count = ValueNotifier(0);

  void increment() {
    count.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text(&apos;Counter&apos;)),
      body: Center(child: ValueListenableBuilder(
        valueListenable: count,
        builder: (context, value, child) {
          return Text(&apos;Count: $value&apos;);
        }
      )),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;FlutterSignal&lt;/h2&gt;
&lt;p&gt;Using the &lt;a href=&quot;https://pub.dev/packages/signals&quot;&gt;signals&lt;/a&gt; package we can upgrade ValueNotifier to a &lt;a href=&quot;https://preactjs.com/guide/v10/signals/&quot;&gt;signal backed implmentation&lt;/a&gt; which uses a reactive graph based on a push / pull architecture.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:signals/signals_flutter.dart&apos;;

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State&amp;lt;Counter&amp;gt; createState() =&amp;gt; _CounterState();
}

class _CounterState extends State&amp;lt;Counter&amp;gt; {
  final count = signal(0);

  void increment() {
    count.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text(&apos;Counter&apos;)),
      body: Center(
        child: ValueListenableBuilder(
          valueListenable: count,
          builder: (context, value, child) {
            return Text(&apos;Count: $value&apos;);
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Signals created after &lt;strong&gt;6.0.0&lt;/strong&gt; also implement ValueNotifier so you can easily migrate them without changing any other code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Instead of ValueListenableBuilder we can use the Watch widget or .watch(context) extension.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:signals/signals_flutter.dart&apos;;

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State&amp;lt;Counter&amp;gt; createState() =&amp;gt; _CounterState();
}

class _CounterState extends State&amp;lt;Counter&amp;gt; {
  final count = signal(0);

  void increment() {
    count.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text(&apos;Counter&apos;)),
      body: Center(
        child: Text(&apos;Count: ${count.watch(context)}&apos;),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;flutter_hooks&lt;/h2&gt;
&lt;p&gt;Using &lt;a href=&quot;https://pub.dev/packages/flutter_hooks&quot;&gt;Flutter Hooks&lt;/a&gt; we can reduce boilerplate of StatefulWidget by switching to a HookWidget. With &lt;strong&gt;useState&lt;/strong&gt; we can define the state directly in the build method and easily share them across widgets.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter_hooks/flutter_hooks.dart&apos;;

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends HookWidget {
  const Counter({super.key});

  @override
  Widget build(BuildContext context) {
    final count = useState(0);
    return Scaffold(
      appBar: AppBar(title: const Text(&apos;Counter&apos;)),
      body: Center(child: Text(&apos;Count: ${count.value}&apos;)),
      floatingActionButton: FloatingActionButton(
        onPressed: () =&amp;gt; count.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;useState&lt;/strong&gt; returns a ValueNotifier that automatically rebuilds the widget on changes&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;signals_hooks&lt;/h2&gt;
&lt;p&gt;Using a new package &lt;a href=&quot;https://pub.dev/packages/signals_hooks&quot;&gt;signals_hooks&lt;/a&gt; we can now define signals in HookWidgets and have the benifits of a reactive graph with shareable lifecycles between widgets.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter_hooks/flutter_hooks.dart&apos;;
import &apos;package:signals_hooks/signals_hooks.dart&apos;;

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends HookWidget {
  const Counter({super.key});

  @override
  Widget build(BuildContext context) {
    final count = useSignal(0);
    final countStr = useComputed(() =&amp;gt; count.value.toString());
    useSignalEffect(() {
      print(&apos;count: $count&apos;);
    });
    return Scaffold(
      appBar: AppBar(title: const Text(&apos;Counter&apos;)),
      body: Center(child: Text(&apos;Count: $countStr&apos;)),
      floatingActionButton: FloatingActionButton(
        onPressed: () =&amp;gt; count.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Server Side Rendering Flutter Apps with RFW</title><link>https://rodydavis.com/flutter/ssr-rfw/</link><guid isPermaLink="true">https://rodydavis.com/flutter/ssr-rfw/</guid><description>Learn how to implement server-side rendering (SSR) in Flutter applications using the rfw package for dynamic UI updates and efficient code delivery, addressing challenges related to user versioning.</description><pubDate>Mon, 20 Jan 2025 02:59:01 GMT</pubDate><content:encoded>&lt;h1&gt;Server Side Rendering Flutter Apps with RFW&lt;/h1&gt;
&lt;p&gt;This post will guide you how to build a Flutter app that takes advantage of Server Side Rendering (SSR) and being able to update UI dynamically.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are new to flutter you can follow this &lt;a href=&quot;https://rodydavis.com/posts/first-flutter-project&quot;&gt;post&lt;/a&gt; on getting started&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This technique will use the &lt;a href=&quot;https://pub.dev/packages/rfw&quot;&gt;rfw&lt;/a&gt; package on the server and client to send binary data via HTTP requests.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; You can find the final source &lt;a href=&quot;https://github.com/rodydavis/flutter_ssr&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Create a new directory called &lt;code&gt;flutter_ssr&lt;/code&gt; and navigate to it in terminal or open it up in your favorite IDE.&lt;/p&gt;
&lt;h2&gt;Approaches to updates&lt;/h2&gt;
&lt;p&gt;If you are in the mobile world then you know how challenging it can be to get all your users on the latest version. Even just having an API or database schema can be very hard to update because of users on older versions (sometimes due to OS limitations).&lt;/p&gt;
&lt;h3&gt;Code Push&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://shorebird.dev/&quot;&gt;Shorebird&lt;/a&gt; takes an interesting approach to delivering updates to the users via Code Push and will update the apps live. This does have an advantage since it will update the UI and logic but what if the content update was only intended for a specific user or set of users?&lt;/p&gt;
&lt;h3&gt;Latest version supported&lt;/h3&gt;
&lt;p&gt;Another approach is to simply have an SLO/Policy that you only support X number of recent releases and that the app will not work on older versions.&lt;/p&gt;
&lt;p&gt;For something like this on mobile you would use &lt;a href=&quot;https://pub.dev/packages/upgrader&quot;&gt;upgrader&lt;/a&gt; via &lt;a href=&quot;https://sparkle-project.org/about/&quot;&gt;AppCast&lt;/a&gt; or &lt;a href=&quot;https://pub.dev/packages/in_app_update&quot;&gt;in_app_update&lt;/a&gt; to use Google Play APIs to update the app in the background or prevent using until updated.&lt;/p&gt;
&lt;p&gt;This has an advantage to know that users will be on the latest or no be supported and allow you to target newer APIs and roll updates easier. This does mean that users will be frustrated by updates more and that older devices may not be supported.&lt;/p&gt;
&lt;h3&gt;Server driven updates&lt;/h3&gt;
&lt;p&gt;Not all apps need to render data on the server and sometimes when building an offline first application you want to do everything local first, but this is for when you need to build a server first application. Here are some examples and use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bank accounts&lt;/li&gt;
&lt;li&gt;Airline / Hotel / Car booking&lt;/li&gt;
&lt;li&gt;Chat applications (Instant messaging)&lt;/li&gt;
&lt;li&gt;Marketing and AB testing&lt;/li&gt;
&lt;li&gt;Database first applications&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these examples does not mean they are server only and in many cases you want to still cache the data locally to still offer a great offline experience.&lt;/p&gt;
&lt;p&gt;With Flutter you are building a runtime that you are shipping to the user as a Single Page Application (SPA) on the web and a mobile/desktop app on the stores. This means you need to ship all the logic and UI for every update.&lt;/p&gt;
&lt;p&gt;This has an advantage for doing more logic on the server and potentially really heavy requests are done server side and just the rendered UI is sent to the client. The client can still cache the response and allow for offline viewing too. These disadvantage here is that the client is expected to communicate with the server at some point and may not be suitable for offline only applications.&lt;/p&gt;
&lt;h2&gt;Remote Flutter Widgets (RFW)&lt;/h2&gt;
&lt;p&gt;The Flutter team has a package for creating widgets on the client and server and sending data necessary to connect them. This package is called &lt;a href=&quot;https://pub.dev/packages/rfw&quot;&gt;rfw&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;While it is possible to ship logic in addition to UI as WASM that is out of scope for this post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The rfw package uses a text format that can be compiled to binary and be used to represent state and dispatch events.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import core;
import material;

widget MaterialShop = Scaffold(
  appBar: AppBar(
    title: Text(text: [&apos;Products&apos;]),
  ),
  body: ListView(
    children: [
      ...for product in data.server.games:
        Product(product: product)
    ],
  ),
);

widget Product = ListTile(
  title: Text(text: args.product.name),
  onTap: event &apos;shop.productSelect&apos; { name: args.product.name, path: args.product.link },
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Multiple widgets can be defined and it may even look similar to the Dart API you are used to in Flutter but it is not quite the same.&lt;/p&gt;
&lt;p&gt;There are not logic branching blocks or conditional rendering but rather is a stateless format capable of updating every frame if needed.&lt;/p&gt;
&lt;p&gt;Take the following Flutter counter app that is generated when you create a new project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({
    Key? key,
    required this.title,
  }) : super(key: key);

  @override
  State&amp;lt;MyHomePage&amp;gt; createState() =&amp;gt; _MyHomePageState();
}

class _MyHomePageState extends State&amp;lt;MyHomePage&amp;gt; {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              &apos;You have pushed the button this many times:&apos;,
            ),
            Text(
              &apos;$_counter&apos;,
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: &apos;Increment&apos;,
        child: const Icon(Icons.add),
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We could represent that in rfw like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import widgets;
import material;

widget root = Scaffold(
  appBar: AppBar(
    title: Text(text: [&apos;Counter Example&apos;]),
    centerTitle: true,
    backgroundColor: data.colorScheme.inversePrimary,
  ),
  body: Center(
    child: Column(
      mainAxisAlignment: &amp;quot;center&amp;quot;,
      children: [
        Text(text: [&amp;quot;You have pushed the button this many times:&amp;quot;]),
        Text(
          text: [data.counter.value],
          style: {
            fontSize: 20.0,
          },
        ),
      ],
    ),
  ),
  floatingActionButton: FloatingActionButton(
     onPressed: event &amp;quot;click&amp;quot; {},
     tooltip: [&amp;quot;Increment&amp;quot;],
     child: Icon(
        icon: 0xe047,
        fontFamily: &apos;MaterialIcons&apos;,
     ),
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You may have noticed some arrays for strings and different ways of defining widgets. That is by design and the API will not be 100% with the Flutter SDK, but with the limitation comes different tradeoffs.&lt;/p&gt;
&lt;p&gt;You can define any custom widgets in your application and only uses the UI you have defined in your design system and the server will only be able to generate UI that you expect.&lt;/p&gt;
&lt;p&gt;It is also possible to create all the UI in the text format with rows, columns, containers and more.&lt;/p&gt;
&lt;h2&gt;Setting up the server&lt;/h2&gt;
&lt;p&gt;For this example we will be using &lt;a href=&quot;https://dartfrog.vgv.dev/docs/overview&quot;&gt;dart_frog&lt;/a&gt; to create the server application.&lt;/p&gt;
&lt;p&gt;In the directory that you created earlier run the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;dart_frog create server
flutter pub add rfw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will generate the server boilerplate for us and add the correct dependencies.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Feel free to delete the test directory for now or update it later to check for the correct response.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Navigate to &lt;code&gt;/server/routes/index.dart&lt;/code&gt; and update the file with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:dart_frog/dart_frog.dart&apos;;
import &apos;package:rfw/formats.dart&apos;;

Response onRequest(RequestContext context) {
  var count = context.request.headers[&apos;COUNTER_VALUE&apos;] ?? &apos;0&apos;;

  if (context.request.method == HttpMethod.post) {
    count = (int.parse(count) + 1).toString();
  }

  return Response.bytes(
    body: encodeLibraryBlob(parseLibraryFile(template)),
    headers: {&apos;COUNTER_VALUE&apos;: count},
  );
}

const template = &apos;&apos;&apos;
import widgets;
import material;

widget root = Scaffold(
  appBar: AppBar(
    title: Text(text: [&apos;Counter Example&apos;]),
    centerTitle: true,
    backgroundColor: data.colorScheme.inversePrimary,
  ),
  body: Center(
    child: Column(
      mainAxisAlignment: &amp;quot;center&amp;quot;,
      children: [
        Text(text: [&amp;quot;You have pushed the button this many times:&amp;quot;]),
        Text(
          text: [data.counter.value],
          style: {
            fontSize: 20.0,
          },
        ),
      ],
    ),
  ),
  floatingActionButton: FloatingActionButton(
     onPressed: event &amp;quot;click&amp;quot; {},
     tooltip: [&amp;quot;Increment&amp;quot;],
     child: Icon(
        icon: 0xe047,
        fontFamily: &apos;MaterialIcons&apos;,
     ),
  ),
);
&apos;&apos;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This takes the rfw text format we defined earlier and adds it to a string template.&lt;/p&gt;
&lt;p&gt;Using that template we can call &lt;code&gt;encodeLibraryBlob(parseLibraryFile(template))&lt;/code&gt; to create a binary representation of the text format.&lt;/p&gt;
&lt;p&gt;We are also checking for the &lt;code&gt;COUNTER_VALUE&lt;/code&gt; from the header to send state to/from the client. Since servers are stateless or easier to scale when they are this will help with not needing a type of session storage or context.&lt;/p&gt;
&lt;p&gt;If you navigate inside the &lt;code&gt;server&lt;/code&gt; directly you can start the dev server that will be needed for the next step:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;dart_frog dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;✓ Running on http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting up the client&lt;/h2&gt;
&lt;p&gt;In a new terminal tab you can navigate to the root of the directory and run the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter create app
flutter pub add rfw http
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will generate the counter app boilerplate and add the correct dependencies for us.&lt;/p&gt;
&lt;p&gt;The rfw package comes with &lt;strong&gt;core&lt;/strong&gt; and &lt;strong&gt;material&lt;/strong&gt; widgets but for this example we will be adding them manually to show how they are being called and created.&lt;/p&gt;
&lt;p&gt;Create and update the following file located att &lt;code&gt;app/lib/rfw/decoders.dart&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:rfw/rfw.dart&apos;;

class CustomArgumentDecoders {
  static ButtonStyle? outlinedButtonStyle(
    DataSource source,
    List&amp;lt;Object&amp;gt; key,
    BuildContext context,
  ) {
    if (!source.isMap(key)) {
      return null;
    }
    return OutlinedButton.styleFrom(
      foregroundColor: ArgumentDecoders.color(source, [&apos;foregroundColor&apos;]),
      backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
      disabledForegroundColor:
          ArgumentDecoders.color(source, [...key, &apos;disabledForegroundColor&apos;]),
      disabledBackgroundColor:
          ArgumentDecoders.color(source, [...key, &apos;disabledBackgroundColor&apos;]),
      shadowColor: ArgumentDecoders.color(source, [...key, &apos;shadowColor&apos;]),
      surfaceTintColor:
          ArgumentDecoders.color(source, [...key, &apos;surfaceTintColor&apos;]),
      elevation: source.v&amp;lt;double&amp;gt;([&apos;elevation&apos;]),
      textStyle: ArgumentDecoders.textStyle(source, [...key, &apos;textStyle&apos;]),
      padding: ArgumentDecoders.edgeInsets(source, [...key, &apos;padding&apos;]),
      minimumSize: CustomArgumentDecoders.size(source, [...key, &apos;minimumSize&apos;]),
      fixedSize: CustomArgumentDecoders.size(source, [...key, &apos;fixedSize&apos;]),
      maximumSize: CustomArgumentDecoders.size(source, [...key, &apos;maximumSize&apos;]),
      side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]),
      shape: CustomArgumentDecoders.outlinedBorder(source, [...key, &apos;shape&apos;]),
      enabledMouseCursor: CustomArgumentDecoders.mouseCursor(
          source, [...key, &apos;enabledMouseCursor&apos;]),
      disabledMouseCursor: CustomArgumentDecoders.mouseCursor(
          source, [...key, &apos;disabledMouseCursor&apos;]),
      visualDensity:
          ArgumentDecoders.visualDensity(source, [...key, &apos;visualDensity&apos;]),
      tapTargetSize: ArgumentDecoders.enumValue&amp;lt;MaterialTapTargetSize&amp;gt;(
              MaterialTapTargetSize.values,
              source,
              [...key, &apos;tapTargetSize&apos;]) ??
          MaterialTapTargetSize.shrinkWrap,
      animationDuration: ArgumentDecoders.duration(
          source, [...key, &apos;animationDuration&apos;], context),
      enableFeedback: source.v&amp;lt;bool&amp;gt;([...key, &apos;enableFeedback&apos;]),
      alignment: ArgumentDecoders.alignment(source, [...key, &apos;alignment&apos;]),
    );
  }

  static ButtonStyle? filledButtonStyle(
    DataSource source,
    List&amp;lt;Object&amp;gt; key,
    BuildContext context,
  ) {
    if (!source.isMap(key)) {
      return null;
    }
    return FilledButton.styleFrom(
      foregroundColor: ArgumentDecoders.color(source, [&apos;foregroundColor&apos;]),
      backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
      disabledForegroundColor:
          ArgumentDecoders.color(source, [...key, &apos;disabledForegroundColor&apos;]),
      disabledBackgroundColor:
          ArgumentDecoders.color(source, [...key, &apos;disabledBackgroundColor&apos;]),
      shadowColor: ArgumentDecoders.color(source, [...key, &apos;shadowColor&apos;]),
      surfaceTintColor:
          ArgumentDecoders.color(source, [...key, &apos;surfaceTintColor&apos;]),
      elevation: source.v&amp;lt;double&amp;gt;([&apos;elevation&apos;]),
      textStyle: ArgumentDecoders.textStyle(source, [...key, &apos;textStyle&apos;]),
      padding: ArgumentDecoders.edgeInsets(source, [...key, &apos;padding&apos;]),
      minimumSize: CustomArgumentDecoders.size(source, [...key, &apos;minimumSize&apos;]),
      fixedSize: CustomArgumentDecoders.size(source, [...key, &apos;fixedSize&apos;]),
      maximumSize: CustomArgumentDecoders.size(source, [...key, &apos;maximumSize&apos;]),
      side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]),
      shape: CustomArgumentDecoders.outlinedBorder(source, [...key, &apos;shape&apos;]),
      enabledMouseCursor: CustomArgumentDecoders.mouseCursor(
          source, [...key, &apos;enabledMouseCursor&apos;]),
      disabledMouseCursor: CustomArgumentDecoders.mouseCursor(
          source, [...key, &apos;disabledMouseCursor&apos;]),
      visualDensity:
          ArgumentDecoders.visualDensity(source, [...key, &apos;visualDensity&apos;]),
      tapTargetSize: ArgumentDecoders.enumValue&amp;lt;MaterialTapTargetSize&amp;gt;(
              MaterialTapTargetSize.values,
              source,
              [...key, &apos;tapTargetSize&apos;]) ??
          MaterialTapTargetSize.shrinkWrap,
      animationDuration: ArgumentDecoders.duration(
          source, [...key, &apos;animationDuration&apos;], context),
      enableFeedback: source.v&amp;lt;bool&amp;gt;([...key, &apos;enableFeedback&apos;]),
      alignment: ArgumentDecoders.alignment(source, [...key, &apos;alignment&apos;]),
    );
  }

  static ButtonStyle? textButtonStyle(
    DataSource source,
    List&amp;lt;Object&amp;gt; key,
    BuildContext context,
  ) {
    if (!source.isMap(key)) {
      return null;
    }
    return TextButton.styleFrom(
      foregroundColor: ArgumentDecoders.color(source, [&apos;foregroundColor&apos;]),
      backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
      disabledForegroundColor:
          ArgumentDecoders.color(source, [...key, &apos;disabledForegroundColor&apos;]),
      disabledBackgroundColor:
          ArgumentDecoders.color(source, [...key, &apos;disabledBackgroundColor&apos;]),
      shadowColor: ArgumentDecoders.color(source, [...key, &apos;shadowColor&apos;]),
      surfaceTintColor:
          ArgumentDecoders.color(source, [...key, &apos;surfaceTintColor&apos;]),
      elevation: source.v&amp;lt;double&amp;gt;([&apos;elevation&apos;]),
      textStyle: ArgumentDecoders.textStyle(source, [...key, &apos;textStyle&apos;]),
      padding: ArgumentDecoders.edgeInsets(source, [...key, &apos;padding&apos;]),
      minimumSize: CustomArgumentDecoders.size(source, [...key, &apos;minimumSize&apos;]),
      fixedSize: CustomArgumentDecoders.size(source, [...key, &apos;fixedSize&apos;]),
      maximumSize: CustomArgumentDecoders.size(source, [...key, &apos;maximumSize&apos;]),
      side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]),
      shape: CustomArgumentDecoders.outlinedBorder(source, [...key, &apos;shape&apos;]),
      enabledMouseCursor: CustomArgumentDecoders.mouseCursor(
          source, [...key, &apos;enabledMouseCursor&apos;]),
      disabledMouseCursor: CustomArgumentDecoders.mouseCursor(
          source, [...key, &apos;disabledMouseCursor&apos;]),
      visualDensity:
          ArgumentDecoders.visualDensity(source, [...key, &apos;visualDensity&apos;]),
      tapTargetSize: ArgumentDecoders.enumValue&amp;lt;MaterialTapTargetSize&amp;gt;(
              MaterialTapTargetSize.values,
              source,
              [...key, &apos;tapTargetSize&apos;]) ??
          MaterialTapTargetSize.shrinkWrap,
      animationDuration: ArgumentDecoders.duration(
          source, [...key, &apos;animationDuration&apos;], context),
      enableFeedback: source.v&amp;lt;bool&amp;gt;([...key, &apos;enableFeedback&apos;]),
      alignment: ArgumentDecoders.alignment(source, [...key, &apos;alignment&apos;]),
    );
  }

  static EdgeInsets? edgeInsets(DataSource source, List&amp;lt;Object&amp;gt; key) {
    if (!source.isMap(key)) {
      return null;
    }
    final all = source.v&amp;lt;double&amp;gt;([...key, &apos;all&apos;]);
    if (all != null) return EdgeInsets.all(all);
    final vertical = source.v&amp;lt;double&amp;gt;([...key, &apos;vertical&apos;]);
    final horizontal = source.v&amp;lt;double&amp;gt;([...key, &apos;horizontal&apos;]);
    if (vertical != null || horizontal != null) {
      return EdgeInsets.symmetric(
        vertical: vertical ?? 0,
        horizontal: horizontal ?? 0,
      );
    }
    final top = source.v&amp;lt;double&amp;gt;([...key, &apos;top&apos;]);
    final bottom = source.v&amp;lt;double&amp;gt;([...key, &apos;bottom&apos;]);
    final left = source.v&amp;lt;double&amp;gt;([...key, &apos;left&apos;]);
    final right = source.v&amp;lt;double&amp;gt;([...key, &apos;right&apos;]);
    return EdgeInsets.only(
      top: top ?? 0,
      bottom: bottom ?? 0,
      left: left ?? 0,
      right: right ?? 0,
    );
  }

  static Size? size(DataSource source, List&amp;lt;Object&amp;gt; key) {
    if (!source.isMap(key)) {
      return null;
    }
    return Size(
      source.v&amp;lt;double&amp;gt;([...key, &apos;width&apos;]) ?? 0.0,
      source.v&amp;lt;double&amp;gt;([...key, &apos;height&apos;]) ?? 0.0,
    );
  }

  static MouseCursor? mouseCursor(DataSource source, List&amp;lt;Object&amp;gt; key) {
    if (!source.isMap(key)) {
      return null;
    }
    final type = source.v&amp;lt;String&amp;gt;([...key, &apos;type&apos;]);
    final value = source.v&amp;lt;String&amp;gt;([...key, &apos;value&apos;]);
    if (type == &apos;system&apos;) {
      switch (value) {
        case &apos;alias&apos;:
          return SystemMouseCursors.alias;
        case &apos;none&apos;:
          return SystemMouseCursors.none;
        case &apos;basic&apos;:
          return SystemMouseCursors.basic;
        case &apos;click&apos;:
          return SystemMouseCursors.click;
        case &apos;forbidden&apos;:
          return SystemMouseCursors.forbidden;
        case &apos;wait&apos;:
          return SystemMouseCursors.wait;
        case &apos;progress&apos;:
          return SystemMouseCursors.progress;
        case &apos;contextMenu&apos;:
          return SystemMouseCursors.contextMenu;
        case &apos;help&apos;:
          return SystemMouseCursors.help;
        case &apos;text&apos;:
          return SystemMouseCursors.text;
        case &apos;verticalText&apos;:
          return SystemMouseCursors.verticalText;
        case &apos;cell&apos;:
          return SystemMouseCursors.cell;
        case &apos;precise&apos;:
          return SystemMouseCursors.precise;
        case &apos;move&apos;:
          return SystemMouseCursors.move;
        case &apos;grab&apos;:
          return SystemMouseCursors.grab;
        case &apos;grabbing&apos;:
          return SystemMouseCursors.grabbing;
        case &apos;noDrop&apos;:
          return SystemMouseCursors.noDrop;
        case &apos;alias&apos;:
          return SystemMouseCursors.alias;
        case &apos;copy&apos;:
          return SystemMouseCursors.copy;
        case &apos;disappearing&apos;:
          return SystemMouseCursors.disappearing;
        case &apos;allScroll&apos;:
          return SystemMouseCursors.allScroll;
        case &apos;resizeLeftRight&apos;:
          return SystemMouseCursors.resizeLeftRight;
        case &apos;resizeUpDown&apos;:
          return SystemMouseCursors.resizeUpDown;
        case &apos;resizeUpLeftDownRight&apos;:
          return SystemMouseCursors.resizeUpLeftDownRight;
        case &apos;resizeUpRightDownLeft&apos;:
          return SystemMouseCursors.resizeUpRightDownLeft;
        case &apos;resizeUp&apos;:
          return SystemMouseCursors.resizeUp;
        case &apos;resizeDown&apos;:
          return SystemMouseCursors.resizeDown;
        case &apos;resizeLeft&apos;:
          return SystemMouseCursors.resizeLeft;
        case &apos;resizeRight&apos;:
          return SystemMouseCursors.resizeRight;
        case &apos;resizeUpLeft&apos;:
          return SystemMouseCursors.resizeUpLeft;
        case &apos;resizeUpRight&apos;:
          return SystemMouseCursors.resizeUpRight;
        case &apos;resizeDownLeft&apos;:
          return SystemMouseCursors.resizeDownLeft;
        case &apos;resizeDownRight&apos;:
          return SystemMouseCursors.resizeDownRight;
        case &apos;resizeColumn&apos;:
          return SystemMouseCursors.resizeColumn;
        case &apos;resizeRow&apos;:
          return SystemMouseCursors.resizeRow;
        case &apos;zoomIn&apos;:
          return SystemMouseCursors.zoomIn;
        case &apos;zoomOut&apos;:
          return SystemMouseCursors.zoomOut;
        default:
      }
    }
    return null;
  }

  static LinearBorderEdge? linearBorderEdge(
      DataSource source, List&amp;lt;Object&amp;gt; key) {
    if (!source.isMap(key)) {
      return null;
    }
    return LinearBorderEdge(
      size: source.v&amp;lt;double&amp;gt;([...key, &apos;size&apos;]) ?? 1.0,
      alignment: source.v&amp;lt;double&amp;gt;([...key, &apos;alignment&apos;]) ?? 0.0,
    );
  }

  static OutlinedBorder? outlinedBorder(DataSource source, List&amp;lt;Object&amp;gt; key) {
    if (!source.isMap(key)) {
      return null;
    }
    final type = source.v&amp;lt;String&amp;gt;([...key, &apos;type&apos;]);
    switch (type) {
      case &amp;quot;RoundedRectangleBorder&amp;quot;:
        return RoundedRectangleBorder(
          side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]) ??
              BorderSide.none,
          borderRadius:
              ArgumentDecoders.borderRadius(source, [...key, &apos;borderRadius&apos;]) ??
                  BorderRadius.zero,
        );
      case &amp;quot;BeveledRectangleBorder&amp;quot;:
        return BeveledRectangleBorder(
          side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]) ??
              BorderSide.none,
          borderRadius:
              ArgumentDecoders.borderRadius(source, [...key, &apos;borderRadius&apos;]) ??
                  BorderRadius.zero,
        );
      case &amp;quot;ContinuousRectangleBorder&amp;quot;:
        return ContinuousRectangleBorder(
          side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]) ??
              BorderSide.none,
          borderRadius:
              ArgumentDecoders.borderRadius(source, [...key, &apos;borderRadius&apos;]) ??
                  BorderRadius.zero,
        );
      case &amp;quot;CircleBorder&amp;quot;:
        return CircleBorder(
          side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]) ??
              BorderSide.none,
          eccentricity: source.v&amp;lt;double&amp;gt;([...key, &apos;eccentricity&apos;]) ?? 0.0,
        );
      case &amp;quot;LinearBorder&amp;quot;:
        return LinearBorder(
          side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]) ??
              BorderSide.none,
          start: CustomArgumentDecoders.linearBorderEdge(
              source, [...key, &apos;start&apos;]),
          end: CustomArgumentDecoders.linearBorderEdge(source, [...key, &apos;end&apos;]),
          top: CustomArgumentDecoders.linearBorderEdge(source, [...key, &apos;top&apos;]),
          bottom: CustomArgumentDecoders.linearBorderEdge(
              source, [...key, &apos;bottom&apos;]),
        );
      case &amp;quot;OvalBorder&amp;quot;:
        return OvalBorder(
          side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]) ??
              BorderSide.none,
          eccentricity: source.v&amp;lt;double&amp;gt;([...key, &apos;eccentricity&apos;]) ?? 0.0,
        );
      case &amp;quot;StadiumBorder&amp;quot;:
        return StadiumBorder(
          side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]) ??
              BorderSide.none,
        );
      case &amp;quot;StarBorder&amp;quot;:
        return StarBorder(
          side: ArgumentDecoders.borderSide(source, [...key, &apos;side&apos;]) ??
              BorderSide.none,
          points: source.v&amp;lt;double&amp;gt;([...key, &apos;points&apos;]) ?? 5.0,
          innerRadiusRatio:
              source.v&amp;lt;double&amp;gt;([...key, &apos;innerRadiusRatio&apos;]) ?? 0.4,
          pointRounding: source.v&amp;lt;double&amp;gt;([...key, &apos;pointRounding&apos;]) ?? 0.0,
          valleyRounding: source.v&amp;lt;double&amp;gt;([...key, &apos;valleyRounding&apos;]) ?? 0.0,
          rotation: source.v&amp;lt;double&amp;gt;([...key, &apos;rotation&apos;]) ?? 0.0,
          squash: source.v&amp;lt;double&amp;gt;([...key, &apos;squash&apos;]) ?? 0.0,
        );
      default:
        break;
    }
    return null;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates the custom decoders needed for the widgets we are about to define.&lt;/p&gt;
&lt;h3&gt;Defining the core library&lt;/h3&gt;
&lt;p&gt;Create and update the following file located at &lt;code&gt;app/lib/rfw/core.dart&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/gestures.dart&apos;;
import &apos;package:flutter/material.dart&apos;;
import &apos;package:rfw/rfw.dart&apos;;

LocalWidgetLibrary createCoreWidgets() =&amp;gt;
    LocalWidgetLibrary(_coreWidgetsDefinitions);

Map&amp;lt;String, LocalWidgetBuilder&amp;gt; get _coreWidgetsDefinitions =&amp;gt;
    &amp;lt;String, LocalWidgetBuilder&amp;gt;{
      &apos;AnimationDefaults&apos;: (BuildContext context, DataSource source) {
        return AnimationDefaults(
          duration: ArgumentDecoders.duration(source, [&apos;duration&apos;], context),
          curve: ArgumentDecoders.curve(source, [&apos;curve&apos;], context),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;Align&apos;: (BuildContext context, DataSource source) {
        return AnimatedAlign(
          duration: ArgumentDecoders.duration(source, [&apos;duration&apos;], context),
          curve: ArgumentDecoders.curve(source, [&apos;curve&apos;], context),
          alignment: ArgumentDecoders.alignment(source, [&apos;alignment&apos;]) ??
              Alignment.center,
          widthFactor: source.v&amp;lt;double&amp;gt;([&apos;widthFactor&apos;]),
          heightFactor: source.v&amp;lt;double&amp;gt;([&apos;heightFactor&apos;]),
          onEnd: source.voidHandler([&apos;onEnd&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;AspectRatio&apos;: (BuildContext context, DataSource source) {
        return AspectRatio(
          aspectRatio: source.v&amp;lt;double&amp;gt;([&apos;aspectRatio&apos;]) ?? 1.0,
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;Center&apos;: (BuildContext context, DataSource source) {
        return Center(
          widthFactor: source.v&amp;lt;double&amp;gt;([&apos;widthFactor&apos;]),
          heightFactor: source.v&amp;lt;double&amp;gt;([&apos;heightFactor&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;ColoredBox&apos;: (BuildContext context, DataSource source) {
        return ColoredBox(
          color: ArgumentDecoders.color(source, [&apos;color&apos;]) ??
              const Color(0xFF000000),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;Column&apos;: (BuildContext context, DataSource source) {
        return Column(
          mainAxisAlignment: ArgumentDecoders.enumValue&amp;lt;MainAxisAlignment&amp;gt;(
                  MainAxisAlignment.values, source, [&apos;mainAxisAlignment&apos;]) ??
              MainAxisAlignment.start,
          mainAxisSize: ArgumentDecoders.enumValue&amp;lt;MainAxisSize&amp;gt;(
                  MainAxisSize.values, source, [&apos;mainAxisSize&apos;]) ??
              MainAxisSize.max,
          crossAxisAlignment: ArgumentDecoders.enumValue&amp;lt;CrossAxisAlignment&amp;gt;(
                  CrossAxisAlignment.values, source, [&apos;crossAxisAlignment&apos;]) ??
              CrossAxisAlignment.center,
          textDirection: ArgumentDecoders.enumValue&amp;lt;TextDirection&amp;gt;(
              TextDirection.values, source, [&apos;textDirection&apos;]),
          verticalDirection: ArgumentDecoders.enumValue&amp;lt;VerticalDirection&amp;gt;(
                  VerticalDirection.values, source, [&apos;verticalDirection&apos;]) ??
              VerticalDirection.down,
          textBaseline: ArgumentDecoders.enumValue&amp;lt;TextBaseline&amp;gt;(
              TextBaseline.values, source, [&apos;textBaseline&apos;]),
          children: source.childList([&apos;children&apos;]),
        );
      },
      &apos;Container&apos;: (BuildContext context, DataSource source) {
        return AnimatedContainer(
          duration: ArgumentDecoders.duration(source, [&apos;duration&apos;], context),
          curve: ArgumentDecoders.curve(source, [&apos;curve&apos;], context),
          alignment: ArgumentDecoders.alignment(source, [&apos;alignment&apos;]),
          padding: ArgumentDecoders.edgeInsets(source, [&apos;padding&apos;]),
          color: ArgumentDecoders.color(source, [&apos;color&apos;]),
          decoration: ArgumentDecoders.decoration(source, [&apos;decoration&apos;]),
          foregroundDecoration:
              ArgumentDecoders.decoration(source, [&apos;foregroundDecoration&apos;]),
          width: source.v&amp;lt;double&amp;gt;([&apos;width&apos;]),
          height: source.v&amp;lt;double&amp;gt;([&apos;height&apos;]),
          constraints: ArgumentDecoders.boxConstraints(source, [&apos;constraints&apos;]),
          margin: ArgumentDecoders.edgeInsets(source, [&apos;margin&apos;]),
          transform: ArgumentDecoders.matrix(source, [&apos;transform&apos;]),
          transformAlignment:
              ArgumentDecoders.alignment(source, [&apos;transformAlignment&apos;]),
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          onEnd: source.voidHandler([&apos;onEnd&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;DefaultTextStyle&apos;: (BuildContext context, DataSource source) {
        return AnimatedDefaultTextStyle(
          duration: ArgumentDecoders.duration(source, [&apos;duration&apos;], context),
          curve: ArgumentDecoders.curve(source, [&apos;curve&apos;], context),
          style: ArgumentDecoders.textStyle(source, [&apos;style&apos;]) ??
              const TextStyle(),
          textAlign: ArgumentDecoders.enumValue&amp;lt;TextAlign&amp;gt;(
              TextAlign.values, source, [&apos;textAlign&apos;]),
          softWrap: source.v&amp;lt;bool&amp;gt;([&apos;softWrap&apos;]) ?? true,
          overflow: ArgumentDecoders.enumValue&amp;lt;TextOverflow&amp;gt;(
                  TextOverflow.values, source, [&apos;overflow&apos;]) ??
              TextOverflow.clip,
          maxLines: source.v&amp;lt;int&amp;gt;([&apos;maxLines&apos;]),
          textWidthBasis: ArgumentDecoders.enumValue&amp;lt;TextWidthBasis&amp;gt;(
                  TextWidthBasis.values, source, [&apos;textWidthBasis&apos;]) ??
              TextWidthBasis.parent,
          textHeightBehavior: ArgumentDecoders.textHeightBehavior(
              source, [&apos;textHeightBehavior&apos;]),
          onEnd: source.voidHandler([&apos;onEnd&apos;]),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;Directionality&apos;: (BuildContext context, DataSource source) {
        return Directionality(
          textDirection: ArgumentDecoders.enumValue&amp;lt;TextDirection&amp;gt;(
                  TextDirection.values, source, [&apos;textDirection&apos;]) ??
              TextDirection.ltr,
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;Expanded&apos;: (BuildContext context, DataSource source) {
        return Expanded(
          flex: source.v&amp;lt;int&amp;gt;([&apos;flex&apos;]) ?? 1,
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;FittedBox&apos;: (BuildContext context, DataSource source) {
        return FittedBox(
          fit: ArgumentDecoders.enumValue&amp;lt;BoxFit&amp;gt;(
                  BoxFit.values, source, [&apos;fit&apos;]) ??
              BoxFit.contain,
          alignment: ArgumentDecoders.alignment(source, [&apos;alignment&apos;]) ??
              Alignment.center,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;FractionallySizedBox&apos;: (BuildContext context, DataSource source) {
        return FractionallySizedBox(
          alignment: ArgumentDecoders.alignment(source, [&apos;alignment&apos;]) ??
              Alignment.center,
          widthFactor: source.v&amp;lt;double&amp;gt;([&apos;widthFactor&apos;]),
          heightFactor: source.v&amp;lt;double&amp;gt;([&apos;heightFactor&apos;]),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;GestureDetector&apos;: (BuildContext context, DataSource source) {
        return GestureDetector(
          onTap: source.voidHandler([&apos;onTap&apos;]),
          onTapDown: source.handler([&apos;onTapDown&apos;],
              (VoidCallback trigger) =&amp;gt; (TapDownDetails details) =&amp;gt; trigger()),
          onTapUp: source.handler([&apos;onTapUp&apos;],
              (VoidCallback trigger) =&amp;gt; (TapUpDetails details) =&amp;gt; trigger()),
          onTapCancel: source.voidHandler([&apos;onTapCancel&apos;]),
          onDoubleTap: source.voidHandler([&apos;onDoubleTap&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          behavior: ArgumentDecoders.enumValue&amp;lt;HitTestBehavior&amp;gt;(
              HitTestBehavior.values, source, [&apos;behavior&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;GridView&apos;: (BuildContext context, DataSource source) {
        return GridView.builder(
          scrollDirection: ArgumentDecoders.enumValue&amp;lt;Axis&amp;gt;(
                  Axis.values, source, [&apos;scrollDirection&apos;]) ??
              Axis.vertical,
          reverse: source.v&amp;lt;bool&amp;gt;([&apos;reverse&apos;]) ?? false,
          primary: source.v&amp;lt;bool&amp;gt;([&apos;primary&apos;]),
          shrinkWrap: source.v&amp;lt;bool&amp;gt;([&apos;shrinkWrap&apos;]) ?? false,
          padding: ArgumentDecoders.edgeInsets(source, [&apos;padding&apos;]),
          gridDelegate:
              ArgumentDecoders.gridDelegate(source, [&apos;gridDelegate&apos;]) ??
                  const SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 2),
          itemBuilder: (BuildContext context, int index) =&amp;gt;
              source.child([&apos;children&apos;, index]),
          itemCount: source.length([&apos;children&apos;]),
          addAutomaticKeepAlives:
              source.v&amp;lt;bool&amp;gt;([&apos;addAutomaticKeepAlives&apos;]) ?? true,
          addRepaintBoundaries:
              source.v&amp;lt;bool&amp;gt;([&apos;addRepaintBoundaries&apos;]) ?? true,
          addSemanticIndexes: source.v&amp;lt;bool&amp;gt;([&apos;addSemanticIndexes&apos;]) ?? true,
          cacheExtent: source.v&amp;lt;double&amp;gt;([&apos;cacheExtent&apos;]),
          semanticChildCount: source.v&amp;lt;int&amp;gt;([&apos;semanticChildCount&apos;]),
          dragStartBehavior: ArgumentDecoders.enumValue&amp;lt;DragStartBehavior&amp;gt;(
                  DragStartBehavior.values, source, [&apos;dragStartBehavior&apos;]) ??
              DragStartBehavior.start,
          keyboardDismissBehavior:
              ArgumentDecoders.enumValue&amp;lt;ScrollViewKeyboardDismissBehavior&amp;gt;(
                      ScrollViewKeyboardDismissBehavior.values,
                      source,
                      [&apos;keyboardDismissBehavior&apos;]) ??
                  ScrollViewKeyboardDismissBehavior.manual,
          restorationId: source.v&amp;lt;String&amp;gt;([&apos;restorationId&apos;]),
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.hardEdge,
        );
      },
      &apos;Icon&apos;: (BuildContext context, DataSource source) {
        return Icon(
          ArgumentDecoders.iconData(source, []) ?? Icons.flutter_dash,
          size: source.v&amp;lt;double&amp;gt;([&apos;size&apos;]),
          color: ArgumentDecoders.color(source, [&apos;color&apos;]),
          semanticLabel: source.v&amp;lt;String&amp;gt;([&apos;semanticLabel&apos;]),
          textDirection: ArgumentDecoders.enumValue&amp;lt;TextDirection&amp;gt;(
              TextDirection.values, source, [&apos;textDirection&apos;]),
        );
      },
      &apos;IconTheme&apos;: (BuildContext context, DataSource source) {
        return IconTheme(
          data: ArgumentDecoders.iconThemeData(source, []) ??
              const IconThemeData(),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;IntrinsicHeight&apos;: (BuildContext context, DataSource source) {
        return IntrinsicHeight(
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;IntrinsicWidth&apos;: (BuildContext context, DataSource source) {
        return IntrinsicWidth(
          stepWidth: source.v&amp;lt;double&amp;gt;([&apos;width&apos;]),
          stepHeight: source.v&amp;lt;double&amp;gt;([&apos;height&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;Image&apos;: (BuildContext context, DataSource source) {
        return Image(
          image: ArgumentDecoders.imageProvider(source, []) ??
              const AssetImage(&apos;error.png&apos;),
          semanticLabel: source.v&amp;lt;String&amp;gt;([&apos;semanticLabel&apos;]),
          excludeFromSemantics:
              source.v&amp;lt;bool&amp;gt;([&apos;excludeFromSemantics&apos;]) ?? false,
          width: source.v&amp;lt;double&amp;gt;([&apos;width&apos;]),
          height: source.v&amp;lt;double&amp;gt;([&apos;height&apos;]),
          color: ArgumentDecoders.color(source, [&apos;color&apos;]),
          colorBlendMode: ArgumentDecoders.enumValue&amp;lt;BlendMode&amp;gt;(
              BlendMode.values, source, [&apos;blendMode&apos;]),
          fit: ArgumentDecoders.enumValue&amp;lt;BoxFit&amp;gt;(
              BoxFit.values, source, [&apos;fit&apos;]),
          alignment: ArgumentDecoders.alignment(source, [&apos;alignment&apos;]) ??
              Alignment.center,
          repeat: ArgumentDecoders.enumValue&amp;lt;ImageRepeat&amp;gt;(
                  ImageRepeat.values, source, [&apos;repeat&apos;]) ??
              ImageRepeat.noRepeat,
          centerSlice: ArgumentDecoders.rect(source, [&apos;centerSlice&apos;]),
          matchTextDirection: source.v&amp;lt;bool&amp;gt;([&apos;matchTextDirection&apos;]) ?? false,
          gaplessPlayback: source.v&amp;lt;bool&amp;gt;([&apos;gaplessPlayback&apos;]) ?? false,
          isAntiAlias: source.v&amp;lt;bool&amp;gt;([&apos;isAntiAlias&apos;]) ?? false,
          filterQuality: ArgumentDecoders.enumValue&amp;lt;FilterQuality&amp;gt;(
                  FilterQuality.values, source, [&apos;filterQuality&apos;]) ??
              FilterQuality.low,
        );
      },
      &apos;ListBody&apos;: (BuildContext context, DataSource source) {
        return ListBody(
          mainAxis: ArgumentDecoders.enumValue&amp;lt;Axis&amp;gt;(
                  Axis.values, source, [&apos;mainAxis&apos;]) ??
              Axis.vertical,
          reverse: source.v&amp;lt;bool&amp;gt;([&apos;reverse&apos;]) ?? false,
          children: source.childList([&apos;children&apos;]),
        );
      },
      &apos;ListView&apos;: (BuildContext context, DataSource source) {
        return ListView.builder(
          scrollDirection: ArgumentDecoders.enumValue&amp;lt;Axis&amp;gt;(
                  Axis.values, source, [&apos;scrollDirection&apos;]) ??
              Axis.vertical,
          reverse: source.v&amp;lt;bool&amp;gt;([&apos;reverse&apos;]) ?? false,
          primary: source.v&amp;lt;bool&amp;gt;([&apos;primary&apos;]),
          shrinkWrap: source.v&amp;lt;bool&amp;gt;([&apos;shrinkWrap&apos;]) ?? false,
          padding: ArgumentDecoders.edgeInsets(source, [&apos;padding&apos;]),
          itemExtent: source.v&amp;lt;double&amp;gt;([&apos;itemExtent&apos;]),
          prototypeItem: source.optionalChild([&apos;prototypeItem&apos;]),
          itemCount: source.length([&apos;children&apos;]),
          itemBuilder: (BuildContext context, int index) =&amp;gt;
              source.child([&apos;children&apos;, index]),
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.hardEdge,
          addAutomaticKeepAlives:
              source.v&amp;lt;bool&amp;gt;([&apos;addAutomaticKeepAlives&apos;]) ?? true,
          addRepaintBoundaries:
              source.v&amp;lt;bool&amp;gt;([&apos;addRepaintBoundaries&apos;]) ?? true,
          addSemanticIndexes: source.v&amp;lt;bool&amp;gt;([&apos;addSemanticIndexes&apos;]) ?? true,
          cacheExtent: source.v&amp;lt;double&amp;gt;([&apos;cacheExtent&apos;]),
          semanticChildCount: source.v&amp;lt;int&amp;gt;([&apos;semanticChildCount&apos;]),
          dragStartBehavior: ArgumentDecoders.enumValue&amp;lt;DragStartBehavior&amp;gt;(
                  DragStartBehavior.values, source, [&apos;dragStartBehavior&apos;]) ??
              DragStartBehavior.start,
          keyboardDismissBehavior:
              ArgumentDecoders.enumValue&amp;lt;ScrollViewKeyboardDismissBehavior&amp;gt;(
                      ScrollViewKeyboardDismissBehavior.values,
                      source,
                      [&apos;keyboardDismissBehavior&apos;]) ??
                  ScrollViewKeyboardDismissBehavior.manual,
          restorationId: source.v&amp;lt;String&amp;gt;([&apos;restorationId&apos;]),
        );
      },
      &apos;Opacity&apos;: (BuildContext context, DataSource source) {
        return AnimatedOpacity(
          duration: ArgumentDecoders.duration(source, [&apos;duration&apos;], context),
          curve: ArgumentDecoders.curve(source, [&apos;curve&apos;], context),
          opacity: source.v&amp;lt;double&amp;gt;([&apos;opacity&apos;]) ?? 0.0,
          onEnd: source.voidHandler([&apos;onEnd&apos;]),
          alwaysIncludeSemantics:
              source.v&amp;lt;bool&amp;gt;([&apos;alwaysIncludeSemantics&apos;]) ?? true,
        );
      },
      &apos;Padding&apos;: (BuildContext context, DataSource source) {
        return AnimatedPadding(
          duration: ArgumentDecoders.duration(source, [&apos;duration&apos;], context),
          curve: ArgumentDecoders.curve(source, [&apos;curve&apos;], context),
          padding: ArgumentDecoders.edgeInsets(source, [&apos;padding&apos;]) ??
              EdgeInsets.zero,
          onEnd: source.voidHandler([&apos;onEnd&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;Placeholder&apos;: (BuildContext context, DataSource source) {
        return Placeholder(
          color: ArgumentDecoders.color(source, [&apos;color&apos;]) ??
              const Color(0xFF455A64),
          strokeWidth: source.v&amp;lt;double&amp;gt;([&apos;strokeWidth&apos;]) ?? 2.0,
          fallbackWidth: source.v&amp;lt;double&amp;gt;([&apos;placeholderWidth&apos;]) ?? 400.0,
          fallbackHeight: source.v&amp;lt;double&amp;gt;([&apos;placeholderHeight&apos;]) ?? 400.0,
        );
      },
      &apos;Positioned&apos;: (BuildContext context, DataSource source) {
        return AnimatedPositionedDirectional(
          duration: ArgumentDecoders.duration(source, [&apos;duration&apos;], context),
          curve: ArgumentDecoders.curve(source, [&apos;curve&apos;], context),
          start: source.v&amp;lt;double&amp;gt;([&apos;start&apos;]),
          top: source.v&amp;lt;double&amp;gt;([&apos;top&apos;]),
          end: source.v&amp;lt;double&amp;gt;([&apos;end&apos;]),
          bottom: source.v&amp;lt;double&amp;gt;([&apos;bottom&apos;]),
          width: source.v&amp;lt;double&amp;gt;([&apos;width&apos;]),
          height: source.v&amp;lt;double&amp;gt;([&apos;height&apos;]),
          onEnd: source.voidHandler([&apos;onEnd&apos;]),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;Rotation&apos;: (BuildContext context, DataSource source) {
        return AnimatedRotation(
          duration: ArgumentDecoders.duration(source, [&apos;duration&apos;], context),
          curve: ArgumentDecoders.curve(source, [&apos;curve&apos;], context),
          turns: source.v&amp;lt;double&amp;gt;([&apos;turns&apos;]) ?? 0.0,
          alignment: (ArgumentDecoders.alignment(source, [&apos;alignment&apos;]) ??
                  Alignment.center)
              .resolve(Directionality.of(context)),
          filterQuality: ArgumentDecoders.enumValue&amp;lt;FilterQuality&amp;gt;(
              FilterQuality.values, source, [&apos;filterQuality&apos;]),
          onEnd: source.voidHandler([&apos;onEnd&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;Row&apos;: (BuildContext context, DataSource source) {
        return Row(
          mainAxisAlignment: ArgumentDecoders.enumValue&amp;lt;MainAxisAlignment&amp;gt;(
                  MainAxisAlignment.values, source, [&apos;mainAxisAlignment&apos;]) ??
              MainAxisAlignment.start,
          mainAxisSize: ArgumentDecoders.enumValue&amp;lt;MainAxisSize&amp;gt;(
                  MainAxisSize.values, source, [&apos;mainAxisSize&apos;]) ??
              MainAxisSize.max,
          crossAxisAlignment: ArgumentDecoders.enumValue&amp;lt;CrossAxisAlignment&amp;gt;(
                  CrossAxisAlignment.values, source, [&apos;crossAxisAlignment&apos;]) ??
              CrossAxisAlignment.center,
          textDirection: ArgumentDecoders.enumValue&amp;lt;TextDirection&amp;gt;(
              TextDirection.values, source, [&apos;textDirection&apos;]),
          verticalDirection: ArgumentDecoders.enumValue&amp;lt;VerticalDirection&amp;gt;(
                  VerticalDirection.values, source, [&apos;verticalDirection&apos;]) ??
              VerticalDirection.down,
          textBaseline: ArgumentDecoders.enumValue&amp;lt;TextBaseline&amp;gt;(
              TextBaseline.values, source, [&apos;textBaseline&apos;]),
          children: source.childList([&apos;children&apos;]),
        );
      },
      &apos;SafeArea&apos;: (BuildContext context, DataSource source) {
        return SafeArea(
          left: source.v&amp;lt;bool&amp;gt;([&apos;left&apos;]) ?? true,
          top: source.v&amp;lt;bool&amp;gt;([&apos;top&apos;]) ?? true,
          right: source.v&amp;lt;bool&amp;gt;([&apos;right&apos;]) ?? true,
          bottom: source.v&amp;lt;bool&amp;gt;([&apos;bottom&apos;]) ?? true,
          minimum: (ArgumentDecoders.edgeInsets(source, [&apos;minimum&apos;]) ??
                  EdgeInsets.zero)
              .resolve(Directionality.of(context)),
          maintainBottomViewPadding:
              source.v&amp;lt;bool&amp;gt;([&apos;maintainBottomViewPadding&apos;]) ?? false,
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;Scale&apos;: (BuildContext context, DataSource source) {
        return AnimatedScale(
          duration: ArgumentDecoders.duration(source, [&apos;duration&apos;], context),
          curve: ArgumentDecoders.curve(source, [&apos;curve&apos;], context),
          scale: source.v&amp;lt;double&amp;gt;([&apos;scale&apos;]) ?? 1.0,
          alignment: (ArgumentDecoders.alignment(source, [&apos;alignment&apos;]) ??
                  Alignment.center)
              .resolve(Directionality.of(context)),
          filterQuality: ArgumentDecoders.enumValue&amp;lt;FilterQuality&amp;gt;(
              FilterQuality.values, source, [&apos;filterQuality&apos;]),
          onEnd: source.voidHandler([&apos;onEnd&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;SingleChildScrollView&apos;: (BuildContext context, DataSource source) {
        return SingleChildScrollView(
          scrollDirection: ArgumentDecoders.enumValue&amp;lt;Axis&amp;gt;(
                  Axis.values, source, [&apos;scrollDirection&apos;]) ??
              Axis.vertical,
          reverse: source.v&amp;lt;bool&amp;gt;([&apos;reverse&apos;]) ?? false,
          padding: ArgumentDecoders.edgeInsets(source, [&apos;padding&apos;]),
          primary: source.v&amp;lt;bool&amp;gt;([&apos;primary&apos;]) ?? true,
          dragStartBehavior: ArgumentDecoders.enumValue&amp;lt;DragStartBehavior&amp;gt;(
                  DragStartBehavior.values, source, [&apos;dragStartBehavior&apos;]) ??
              DragStartBehavior.start,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.hardEdge,
          restorationId: source.v&amp;lt;String&amp;gt;([&apos;restorationId&apos;]),
          keyboardDismissBehavior:
              ArgumentDecoders.enumValue&amp;lt;ScrollViewKeyboardDismissBehavior&amp;gt;(
                      ScrollViewKeyboardDismissBehavior.values,
                      source,
                      [&apos;keyboardDismissBehavior&apos;]) ??
                  ScrollViewKeyboardDismissBehavior.manual,
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;SizedBox&apos;: (BuildContext context, DataSource source) {
        return SizedBox(
          width: source.v&amp;lt;double&amp;gt;([&apos;width&apos;]),
          height: source.v&amp;lt;double&amp;gt;([&apos;height&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;SizedBoxExpand&apos;: (BuildContext context, DataSource source) {
        return SizedBox.expand(
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;SizedBoxShrink&apos;: (BuildContext context, DataSource source) {
        return SizedBox.shrink(
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;Spacer&apos;: (BuildContext context, DataSource source) {
        return Spacer(
          flex: source.v&amp;lt;int&amp;gt;([&apos;flex&apos;]) ?? 1,
        );
      },
      &apos;Stack&apos;: (BuildContext context, DataSource source) {
        return Stack(
          alignment: ArgumentDecoders.alignment(source, [&apos;alignment&apos;]) ??
              AlignmentDirectional.topStart,
          textDirection: ArgumentDecoders.enumValue&amp;lt;TextDirection&amp;gt;(
              TextDirection.values, source, [&apos;textDirection&apos;]),
          fit: ArgumentDecoders.enumValue&amp;lt;StackFit&amp;gt;(
                  StackFit.values, source, [&apos;fit&apos;]) ??
              StackFit.loose,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.hardEdge,
          children: source.childList([&apos;children&apos;]),
        );
      },
      &apos;Text&apos;: (BuildContext context, DataSource source) {
        String? text = source.v&amp;lt;String&amp;gt;([&apos;text&apos;]);
        if (text == null) {
          final StringBuffer builder = StringBuffer();
          final int count = source.length([&apos;text&apos;]);
          for (int index = 0; index &amp;lt; count; index += 1) {
            builder.write(source.v&amp;lt;String&amp;gt;([&apos;text&apos;, index]) ?? &apos;&apos;);
          }
          text = builder.toString();
        }
        return Text(
          text,
          style: ArgumentDecoders.textStyle(source, [&apos;style&apos;]),
          strutStyle: ArgumentDecoders.strutStyle(source, [&apos;strutStyle&apos;]),
          textAlign: ArgumentDecoders.enumValue&amp;lt;TextAlign&amp;gt;(
              TextAlign.values, source, [&apos;textAlign&apos;]),
          textDirection: ArgumentDecoders.enumValue&amp;lt;TextDirection&amp;gt;(
              TextDirection.values, source, [&apos;textDirection&apos;]),
          locale: ArgumentDecoders.locale(source, [&apos;locale&apos;]),
          softWrap: source.v&amp;lt;bool&amp;gt;([&apos;softWrap&apos;]),
          overflow: ArgumentDecoders.enumValue&amp;lt;TextOverflow&amp;gt;(
              TextOverflow.values, source, [&apos;overflow&apos;]),
          textScaleFactor: source.v&amp;lt;double&amp;gt;([&apos;textScaleFactor&apos;]),
          maxLines: source.v&amp;lt;int&amp;gt;([&apos;maxLines&apos;]),
          semanticsLabel: source.v&amp;lt;String&amp;gt;([&apos;semanticsLabel&apos;]),
          textWidthBasis: ArgumentDecoders.enumValue&amp;lt;TextWidthBasis&amp;gt;(
              TextWidthBasis.values, source, [&apos;textWidthBasis&apos;]),
          textHeightBehavior: ArgumentDecoders.textHeightBehavior(
              source, [&apos;textHeightBehavior&apos;]),
        );
      },
      &apos;Wrap&apos;: (BuildContext context, DataSource source) {
        return Wrap(
          direction: ArgumentDecoders.enumValue&amp;lt;Axis&amp;gt;(
                  Axis.values, source, [&apos;direction&apos;]) ??
              Axis.horizontal,
          alignment: ArgumentDecoders.enumValue&amp;lt;WrapAlignment&amp;gt;(
                  WrapAlignment.values, source, [&apos;alignment&apos;]) ??
              WrapAlignment.start,
          spacing: source.v&amp;lt;double&amp;gt;([&apos;spacing&apos;]) ?? 0.0,
          runAlignment: ArgumentDecoders.enumValue&amp;lt;WrapAlignment&amp;gt;(
                  WrapAlignment.values, source, [&apos;runAlignment&apos;]) ??
              WrapAlignment.start,
          runSpacing: source.v&amp;lt;double&amp;gt;([&apos;runSpacing&apos;]) ?? 0.0,
          crossAxisAlignment: ArgumentDecoders.enumValue&amp;lt;WrapCrossAlignment&amp;gt;(
                  WrapCrossAlignment.values, source, [&apos;crossAxisAlignment&apos;]) ??
              WrapCrossAlignment.start,
          textDirection: ArgumentDecoders.enumValue&amp;lt;TextDirection&amp;gt;(
              TextDirection.values, source, [&apos;textDirection&apos;]),
          verticalDirection: ArgumentDecoders.enumValue&amp;lt;VerticalDirection&amp;gt;(
                  VerticalDirection.values, source, [&apos;verticalDirection&apos;]) ??
              VerticalDirection.down,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          children: source.childList([&apos;children&apos;]),
        );
      },
    };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This created all the core widgets that are not apart of a design system. Keeping these separate will let you update the design system separately than the core widgets.&lt;/p&gt;
&lt;h3&gt;Defining the Material library&lt;/h3&gt;
&lt;p&gt;Create and update the following file located at &lt;code&gt;app/lib/rfw/material.dart&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/gestures.dart&apos;;
import &apos;package:flutter/material.dart&apos;;
import &apos;package:rfw/rfw.dart&apos;;

import &apos;decoders.dart&apos;;

LocalWidgetLibrary createMaterialWidgets() =&amp;gt;
    LocalWidgetLibrary(_materialWidgetsDefinitions);

Map&amp;lt;String, LocalWidgetBuilder&amp;gt; get _materialWidgetsDefinitions =&amp;gt;
    &amp;lt;String, LocalWidgetBuilder&amp;gt;{
      &apos;AboutListTile&apos;: (context, source) {
        return AboutListTile(
          icon: source.optionalChild([&apos;icon&apos;]),
          applicationName: source.v&amp;lt;String&amp;gt;([&apos;applicationName&apos;]),
          applicationVersion: source.v&amp;lt;String&amp;gt;([&apos;applicationVersion&apos;]),
          applicationIcon: source.optionalChild([&apos;applicationIcon&apos;]),
          applicationLegalese: source.v&amp;lt;String&amp;gt;([&apos;applicationLegalese&apos;]),
          aboutBoxChildren: source.childList([&apos;aboutBoxChildren&apos;]),
          dense: source.v&amp;lt;bool&amp;gt;([&apos;dense&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;AppBar&apos;: (context, source) {
        return AppBar(
          leading: source.optionalChild([&apos;leading&apos;]),
          automaticallyImplyLeading:
              source.v&amp;lt;bool&amp;gt;([&apos;automaticallyImplyLeading&apos;]) ?? true,
          title: source.optionalChild([&apos;title&apos;]),
          actions: source.childList([&apos;actions&apos;]),
          elevation: source.v&amp;lt;double&amp;gt;([&apos;elevation&apos;]),
          shadowColor: ArgumentDecoders.color(source, [&apos;shadowColor&apos;]),
          shape: ArgumentDecoders.shapeBorder(source, [&apos;shape&apos;]),
          backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
          foregroundColor: ArgumentDecoders.color(source, [&apos;foregroundColor&apos;]),
          iconTheme: ArgumentDecoders.iconThemeData(source, [&apos;iconTheme&apos;]),
          actionsIconTheme:
              ArgumentDecoders.iconThemeData(source, [&apos;actionsIconTheme&apos;]),
          primary: source.v&amp;lt;bool&amp;gt;([&apos;primary&apos;]) ?? true,
          centerTitle: source.v&amp;lt;bool&amp;gt;([&apos;centerTitle&apos;]),
          excludeHeaderSemantics:
              source.v&amp;lt;bool&amp;gt;([&apos;excludeHeaderSemantics&apos;]) ?? false,
          titleSpacing: source.v&amp;lt;double&amp;gt;([&apos;titleSpacing&apos;]),
          toolbarOpacity: source.v&amp;lt;double&amp;gt;([&apos;toolbarOpacity&apos;]) ?? 1.0,
          toolbarHeight: source.v&amp;lt;double&amp;gt;([&apos;toolbarHeight&apos;]),
          leadingWidth: source.v&amp;lt;double&amp;gt;([&apos;leadingWidth&apos;]),
          toolbarTextStyle:
              ArgumentDecoders.textStyle(source, [&apos;toolbarTextStyle&apos;]),
          titleTextStyle:
              ArgumentDecoders.textStyle(source, [&apos;titleTextStyle&apos;]),
        );
      },
      &apos;ButtonBar&apos;: (context, source) {
        return ButtonBar(
          alignment: ArgumentDecoders.enumValue&amp;lt;MainAxisAlignment&amp;gt;(
                  MainAxisAlignment.values, source, [&apos;alignment&apos;]) ??
              MainAxisAlignment.start,
          mainAxisSize: ArgumentDecoders.enumValue&amp;lt;MainAxisSize&amp;gt;(
                  MainAxisSize.values, source, [&apos;mainAxisSize&apos;]) ??
              MainAxisSize.max,
          buttonMinWidth: source.v&amp;lt;double&amp;gt;([&apos;buttonMinWidth&apos;]),
          buttonHeight: source.v&amp;lt;double&amp;gt;([&apos;buttonHeight&apos;]),
          buttonPadding: ArgumentDecoders.edgeInsets(source, [&apos;buttonPadding&apos;]),
          buttonAlignedDropdown:
              source.v&amp;lt;bool&amp;gt;([&apos;buttonAlignedDropdown&apos;]) ?? false,
          layoutBehavior: ArgumentDecoders.enumValue&amp;lt;ButtonBarLayoutBehavior&amp;gt;(
              ButtonBarLayoutBehavior.values, source, [&apos;layoutBehavior&apos;]),
          overflowDirection: ArgumentDecoders.enumValue&amp;lt;VerticalDirection&amp;gt;(
              VerticalDirection.values, source, [&apos;overflowDirection&apos;]),
          overflowButtonSpacing: source.v&amp;lt;double&amp;gt;([&apos;overflowButtonSpacing&apos;]),
          children: source.childList([&apos;children&apos;]),
        );
      },
      &apos;Card&apos;: (context, source) {
        return Card(
          color: ArgumentDecoders.color(source, [&apos;color&apos;]),
          shadowColor: ArgumentDecoders.color(source, [&apos;shadowColor&apos;]),
          elevation: source.v&amp;lt;double&amp;gt;([&apos;elevation&apos;]),
          shape: ArgumentDecoders.shapeBorder(source, [&apos;shape&apos;]),
          borderOnForeground: source.v&amp;lt;bool&amp;gt;([&apos;borderOnForeground&apos;]) ?? true,
          margin: ArgumentDecoders.edgeInsets(source, [&apos;margin&apos;]),
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          semanticContainer: source.v&amp;lt;bool&amp;gt;([&apos;semanticContainer&apos;]) ?? true,
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;CircularProgressIndicator&apos;: (context, source) {
        return CircularProgressIndicator(
          value: source.v&amp;lt;double&amp;gt;([&apos;value&apos;]),
          color: ArgumentDecoders.color(source, [&apos;color&apos;]),
          backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
          strokeWidth: source.v&amp;lt;double&amp;gt;([&apos;strokeWidth&apos;]) ?? 4.0,
          semanticsLabel: source.v&amp;lt;String&amp;gt;([&apos;semanticsLabel&apos;]),
          semanticsValue: source.v&amp;lt;String&amp;gt;([&apos;semanticsValue&apos;]),
        );
      },
      &apos;Divider&apos;: (context, source) {
        return Divider(
          height: source.v&amp;lt;double&amp;gt;([&apos;height&apos;]),
          thickness: source.v&amp;lt;double&amp;gt;([&apos;thickness&apos;]),
          indent: source.v&amp;lt;double&amp;gt;([&apos;indent&apos;]),
          endIndent: source.v&amp;lt;double&amp;gt;([&apos;endIndent&apos;]),
          color: ArgumentDecoders.color(source, [&apos;color&apos;]),
        );
      },
      &apos;Drawer&apos;: (context, source) {
        return Drawer(
          elevation: source.v&amp;lt;double&amp;gt;([&apos;elevation&apos;]) ?? 16.0,
          semanticLabel: source.v&amp;lt;String&amp;gt;([&apos;semanticLabel&apos;]),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;DrawerHeader&apos;: (context, source) {
        return DrawerHeader(
          duration: ArgumentDecoders.duration(source, [&apos;duration&apos;], context),
          curve: ArgumentDecoders.curve(source, [&apos;curve&apos;], context),
          decoration: ArgumentDecoders.decoration(source, [&apos;decoration&apos;]),
          margin: ArgumentDecoders.edgeInsets(source, [&apos;margin&apos;]) ??
              const EdgeInsets.only(bottom: 8.0),
          padding: ArgumentDecoders.edgeInsets(source, [&apos;padding&apos;]) ??
              const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0),
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;ElevatedButton&apos;: (context, source) {
        return ElevatedButton(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;InkWell&apos;: (context, source) {
        return InkWell(
          onTap: source.voidHandler([&apos;onTap&apos;]),
          onDoubleTap: source.voidHandler([&apos;onDoubleTap&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          onTapDown: source.handler([&apos;onTapDown&apos;],
              (VoidCallback trigger) =&amp;gt; (TapDownDetails details) =&amp;gt; trigger()),
          onTapCancel: source.voidHandler([&apos;onTapCancel&apos;]),
          radius: source.v&amp;lt;double&amp;gt;([&apos;radius&apos;]),
          borderRadius: ArgumentDecoders.borderRadius(source, [&apos;borderRadius&apos;])
              ?.resolve(Directionality.of(context)),
          customBorder: ArgumentDecoders.shapeBorder(source, [&apos;customBorder&apos;]),
          enableFeedback: source.v&amp;lt;bool&amp;gt;([&apos;enableFeedback&apos;]) ?? true,
          excludeFromSemantics:
              source.v&amp;lt;bool&amp;gt;([&apos;excludeFromSemantics&apos;]) ?? false,
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          child: source.optionalChild([&apos;child&apos;]),
        );
      },
      &apos;LinearProgressIndicator&apos;: (context, source) {
        return LinearProgressIndicator(
          value: source.v&amp;lt;double&amp;gt;([&apos;value&apos;]),
          color: ArgumentDecoders.color(source, [&apos;color&apos;]),
          backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
          minHeight: source.v&amp;lt;double&amp;gt;([&apos;minHeight&apos;]),
          semanticsLabel: source.v&amp;lt;String&amp;gt;([&apos;semanticsLabel&apos;]),
          semanticsValue: source.v&amp;lt;String&amp;gt;([&apos;semanticsValue&apos;]),
        );
      },
      &apos;ListTile&apos;: (context, source) {
        return ListTile(
          leading: source.optionalChild([&apos;leading&apos;]),
          title: source.optionalChild([&apos;title&apos;]),
          subtitle: source.optionalChild([&apos;subtitle&apos;]),
          trailing: source.optionalChild([&apos;trailing&apos;]),
          isThreeLine: source.v&amp;lt;bool&amp;gt;([&apos;isThreeLine&apos;]) ?? false,
          dense: source.v&amp;lt;bool&amp;gt;([&apos;dense&apos;]),
          visualDensity:
              ArgumentDecoders.visualDensity(source, [&apos;visualDensity&apos;]),
          shape: ArgumentDecoders.shapeBorder(source, [&apos;shape&apos;]),
          contentPadding:
              ArgumentDecoders.edgeInsets(source, [&apos;contentPadding&apos;]),
          enabled: source.v&amp;lt;bool&amp;gt;([&apos;enabled&apos;]) ?? true,
          onTap: source.voidHandler([&apos;onTap&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          selected: source.v&amp;lt;bool&amp;gt;([&apos;selected&apos;]) ?? false,
          focusColor: ArgumentDecoders.color(source, [&apos;focusColor&apos;]),
          hoverColor: ArgumentDecoders.color(source, [&apos;hoverColor&apos;]),
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          tileColor: ArgumentDecoders.color(source, [&apos;tileColor&apos;]),
          selectedTileColor:
              ArgumentDecoders.color(source, [&apos;selectedTileColor&apos;]),
          enableFeedback: source.v&amp;lt;bool&amp;gt;([&apos;enableFeedback&apos;]),
          horizontalTitleGap: source.v&amp;lt;double&amp;gt;([&apos;horizontalTitleGap&apos;]),
          minVerticalPadding: source.v&amp;lt;double&amp;gt;([&apos;minVerticalPadding&apos;]),
          minLeadingWidth: source.v&amp;lt;double&amp;gt;([&apos;minLeadingWidth&apos;]),
        );
      },
      &apos;Scaffold&apos;: (context, source) {
        final Widget? appBarWidget = source.optionalChild([&apos;appBar&apos;]);
        final List&amp;lt;Widget&amp;gt; persistentFooterButtons =
            source.childList([&apos;persistentFooterButtons&apos;]);
        return Scaffold(
          appBar: appBarWidget == null
              ? null
              : PreferredSize(
                  preferredSize: Size.fromHeight(
                      source.v&amp;lt;double&amp;gt;([&apos;bottomHeight&apos;]) ?? 56.0),
                  child: appBarWidget,
                ),
          body: source.optionalChild([&apos;body&apos;]),
          floatingActionButton: source.optionalChild([&apos;floatingActionButton&apos;]),
          persistentFooterButtons:
              persistentFooterButtons.isEmpty ? null : persistentFooterButtons,
          drawer: source.optionalChild([&apos;drawer&apos;]),
          endDrawer: source.optionalChild([&apos;endDrawer&apos;]),
          bottomNavigationBar: source.optionalChild([&apos;bottomNavigationBar&apos;]),
          bottomSheet: source.optionalChild([&apos;bottomSheet&apos;]),
          backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
          resizeToAvoidBottomInset:
              source.v&amp;lt;bool&amp;gt;([&apos;resizeToAvoidBottomInset&apos;]),
          primary: source.v&amp;lt;bool&amp;gt;([&apos;primary&apos;]) ?? true,
          drawerDragStartBehavior:
              ArgumentDecoders.enumValue&amp;lt;DragStartBehavior&amp;gt;(
                      DragStartBehavior.values,
                      source,
                      [&apos;drawerDragStartBehavior&apos;]) ??
                  DragStartBehavior.start,
          extendBody: source.v&amp;lt;bool&amp;gt;([&apos;extendBody&apos;]) ?? false,
          extendBodyBehindAppBar:
              source.v&amp;lt;bool&amp;gt;([&apos;extendBodyBehindAppBar&apos;]) ?? false,
          drawerScrimColor:
              ArgumentDecoders.color(source, [&apos;drawerScrimColor&apos;]),
          drawerEdgeDragWidth: source.v&amp;lt;double&amp;gt;([&apos;drawerEdgeDragWidth&apos;]),
          drawerEnableOpenDragGesture:
              source.v&amp;lt;bool&amp;gt;([&apos;drawerEnableOpenDragGesture&apos;]) ?? true,
          endDrawerEnableOpenDragGesture:
              source.v&amp;lt;bool&amp;gt;([&apos;endDrawerEnableOpenDragGesture&apos;]) ?? true,
          restorationId: source.v&amp;lt;String&amp;gt;([&apos;restorationId&apos;]),
        );
      },
      &apos;VerticalDivider&apos;: (context, source) {
        return VerticalDivider(
          width: source.v&amp;lt;double&amp;gt;([&apos;width&apos;]),
          thickness: source.v&amp;lt;double&amp;gt;([&apos;thickness&apos;]),
          indent: source.v&amp;lt;double&amp;gt;([&apos;indent&apos;]),
          endIndent: source.v&amp;lt;double&amp;gt;([&apos;endIndent&apos;]),
          color: ArgumentDecoders.color(source, [&apos;color&apos;]),
        );
      },
      &apos;FilledButton&apos;: (context, source) {
        return FilledButton(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          style: CustomArgumentDecoders.filledButtonStyle(
              source, [&apos;style&apos;], context),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;FilledButtonIcon&apos;: (context, source) {
        return FilledButton.icon(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          style: CustomArgumentDecoders.filledButtonStyle(
              source, [&apos;style&apos;], context),
          icon: source.child([&apos;icon&apos;]),
          label: source.child([&apos;label&apos;]),
        );
      },
      &apos;FilledButtonTonal&apos;: (context, source) {
        return FilledButton.tonal(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          style: CustomArgumentDecoders.filledButtonStyle(
              source, [&apos;style&apos;], context),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;FilledButtonTonalIcon&apos;: (context, source) {
        return FilledButton.tonalIcon(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          style: CustomArgumentDecoders.filledButtonStyle(
              source, [&apos;style&apos;], context),
          icon: source.child([&apos;icon&apos;]),
          label: source.child([&apos;label&apos;]),
        );
      },
      &apos;TextButton&apos;: (context, source) {
        return TextButton(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          style: CustomArgumentDecoders.textButtonStyle(
              source, [&apos;style&apos;], context),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;TextButtonIcon&apos;: (context, source) {
        return TextButton.icon(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          style: CustomArgumentDecoders.textButtonStyle(
              source, [&apos;style&apos;], context),
          icon: source.child([&apos;icon&apos;]),
          label: source.child([&apos;label&apos;]),
        );
      },
      &apos;OutlinedButton&apos;: (context, source) {
        return OutlinedButton(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          style: CustomArgumentDecoders.outlinedButtonStyle(
              source, [&apos;style&apos;], context),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;OutlinedButtonIcon&apos;: (context, source) {
        return OutlinedButton.icon(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          onLongPress: source.voidHandler([&apos;onLongPress&apos;]),
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          style: CustomArgumentDecoders.outlinedButtonStyle(
              source, [&apos;style&apos;], context),
          icon: source.child([&apos;icon&apos;]),
          label: source.child([&apos;label&apos;]),
        );
      },
      &apos;FloatingActionButton&apos;: (context, source) {
        return FloatingActionButton(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          tooltip: source.v&amp;lt;String&amp;gt;([&apos;tooltip&apos;]),
          foregroundColor: ArgumentDecoders.color(source, [&apos;foregroundColor&apos;]),
          backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
          focusColor: ArgumentDecoders.color(source, [&apos;focusColor&apos;]),
          hoverColor: ArgumentDecoders.color(source, [&apos;hoverColor&apos;]),
          splashColor: ArgumentDecoders.color(source, [&apos;splashColor&apos;]),
          heroTag: source.v&amp;lt;String&amp;gt;([&apos;heroTag&apos;]),
          elevation: source.v&amp;lt;double&amp;gt;([&apos;elevation&apos;]),
          focusElevation: source.v&amp;lt;double&amp;gt;([&apos;focusElevation&apos;]),
          hoverElevation: source.v&amp;lt;double&amp;gt;([&apos;hoverElevation&apos;]),
          highlightElevation: source.v&amp;lt;double&amp;gt;([&apos;highlightElevation&apos;]),
          disabledElevation: source.v&amp;lt;double&amp;gt;([&apos;disabledElevation&apos;]),
          mini: source.v&amp;lt;bool&amp;gt;([&apos;mini&apos;]) ?? false,
          shape: ArgumentDecoders.shapeBorder(source, [&apos;shape&apos;]),
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          mouseCursor:
              CustomArgumentDecoders.mouseCursor(source, [&apos;mouseCursor&apos;]),
          materialTapTargetSize:
              ArgumentDecoders.enumValue&amp;lt;MaterialTapTargetSize&amp;gt;(
                  MaterialTapTargetSize.values,
                  source,
                  [&apos;materialTapTargetSize&apos;]),
          isExtended: source.v&amp;lt;bool&amp;gt;([&apos;isExtended&apos;]) ?? false,
          enableFeedback: source.v&amp;lt;bool&amp;gt;([&apos;enableFeedback&apos;]),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;FloatingActionButtonExtended&apos;: (context, source) {
        return FloatingActionButton.extended(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          tooltip: source.v&amp;lt;String&amp;gt;([&apos;tooltip&apos;]),
          foregroundColor: ArgumentDecoders.color(source, [&apos;foregroundColor&apos;]),
          backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
          focusColor: ArgumentDecoders.color(source, [&apos;focusColor&apos;]),
          hoverColor: ArgumentDecoders.color(source, [&apos;hoverColor&apos;]),
          splashColor: ArgumentDecoders.color(source, [&apos;splashColor&apos;]),
          heroTag: source.v&amp;lt;String&amp;gt;([&apos;heroTag&apos;]),
          elevation: source.v&amp;lt;double&amp;gt;([&apos;elevation&apos;]),
          focusElevation: source.v&amp;lt;double&amp;gt;([&apos;focusElevation&apos;]),
          hoverElevation: source.v&amp;lt;double&amp;gt;([&apos;hoverElevation&apos;]),
          highlightElevation: source.v&amp;lt;double&amp;gt;([&apos;highlightElevation&apos;]),
          disabledElevation: source.v&amp;lt;double&amp;gt;([&apos;disabledElevation&apos;]),
          shape: ArgumentDecoders.shapeBorder(source, [&apos;shape&apos;]),
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          mouseCursor:
              CustomArgumentDecoders.mouseCursor(source, [&apos;mouseCursor&apos;]),
          materialTapTargetSize:
              ArgumentDecoders.enumValue&amp;lt;MaterialTapTargetSize&amp;gt;(
                  MaterialTapTargetSize.values,
                  source,
                  [&apos;materialTapTargetSize&apos;]),
          isExtended: source.v&amp;lt;bool&amp;gt;([&apos;isExtended&apos;]) ?? true,
          enableFeedback: source.v&amp;lt;bool&amp;gt;([&apos;enableFeedback&apos;]),
          label: source.child([&apos;label&apos;]),
          icon: source.child([&apos;icon&apos;]),
        );
      },
      &apos;FloatingActionButtonSmall&apos;: (context, source) {
        return FloatingActionButton.small(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          tooltip: source.v&amp;lt;String&amp;gt;([&apos;tooltip&apos;]),
          foregroundColor: ArgumentDecoders.color(source, [&apos;foregroundColor&apos;]),
          backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
          focusColor: ArgumentDecoders.color(source, [&apos;focusColor&apos;]),
          hoverColor: ArgumentDecoders.color(source, [&apos;hoverColor&apos;]),
          splashColor: ArgumentDecoders.color(source, [&apos;splashColor&apos;]),
          heroTag: source.v&amp;lt;String&amp;gt;([&apos;heroTag&apos;]),
          elevation: source.v&amp;lt;double&amp;gt;([&apos;elevation&apos;]),
          focusElevation: source.v&amp;lt;double&amp;gt;([&apos;focusElevation&apos;]),
          hoverElevation: source.v&amp;lt;double&amp;gt;([&apos;hoverElevation&apos;]),
          highlightElevation: source.v&amp;lt;double&amp;gt;([&apos;highlightElevation&apos;]),
          disabledElevation: source.v&amp;lt;double&amp;gt;([&apos;disabledElevation&apos;]),
          shape: ArgumentDecoders.shapeBorder(source, [&apos;shape&apos;]),
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          mouseCursor:
              CustomArgumentDecoders.mouseCursor(source, [&apos;mouseCursor&apos;]),
          materialTapTargetSize:
              ArgumentDecoders.enumValue&amp;lt;MaterialTapTargetSize&amp;gt;(
                  MaterialTapTargetSize.values,
                  source,
                  [&apos;materialTapTargetSize&apos;]),
          enableFeedback: source.v&amp;lt;bool&amp;gt;([&apos;enableFeedback&apos;]),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;FloatingActionButtonLarge&apos;: (context, source) {
        return FloatingActionButton.large(
          onPressed: source.voidHandler([&apos;onPressed&apos;]),
          tooltip: source.v&amp;lt;String&amp;gt;([&apos;tooltip&apos;]),
          foregroundColor: ArgumentDecoders.color(source, [&apos;foregroundColor&apos;]),
          backgroundColor: ArgumentDecoders.color(source, [&apos;backgroundColor&apos;]),
          focusColor: ArgumentDecoders.color(source, [&apos;focusColor&apos;]),
          hoverColor: ArgumentDecoders.color(source, [&apos;hoverColor&apos;]),
          splashColor: ArgumentDecoders.color(source, [&apos;splashColor&apos;]),
          heroTag: source.v&amp;lt;String&amp;gt;([&apos;heroTag&apos;]),
          elevation: source.v&amp;lt;double&amp;gt;([&apos;elevation&apos;]),
          focusElevation: source.v&amp;lt;double&amp;gt;([&apos;focusElevation&apos;]),
          hoverElevation: source.v&amp;lt;double&amp;gt;([&apos;hoverElevation&apos;]),
          highlightElevation: source.v&amp;lt;double&amp;gt;([&apos;highlightElevation&apos;]),
          disabledElevation: source.v&amp;lt;double&amp;gt;([&apos;disabledElevation&apos;]),
          shape: ArgumentDecoders.shapeBorder(source, [&apos;shape&apos;]),
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          autofocus: source.v&amp;lt;bool&amp;gt;([&apos;autofocus&apos;]) ?? false,
          mouseCursor:
              CustomArgumentDecoders.mouseCursor(source, [&apos;mouseCursor&apos;]),
          materialTapTargetSize:
              ArgumentDecoders.enumValue&amp;lt;MaterialTapTargetSize&amp;gt;(
                  MaterialTapTargetSize.values,
                  source,
                  [&apos;materialTapTargetSize&apos;]),
          enableFeedback: source.v&amp;lt;bool&amp;gt;([&apos;enableFeedback&apos;]),
          child: source.child([&apos;child&apos;]),
        );
      },
      &apos;InteractiveViewer&apos;: (context, source) {
        return InteractiveViewer(
          clipBehavior: ArgumentDecoders.enumValue&amp;lt;Clip&amp;gt;(
                  Clip.values, source, [&apos;clipBehavior&apos;]) ??
              Clip.none,
          alignPanAxis: source.v&amp;lt;bool&amp;gt;([&apos;alignPanAxis&apos;]) ?? false,
          panAxis: ArgumentDecoders.enumValue&amp;lt;PanAxis&amp;gt;(
                  PanAxis.values, source, [&apos;panAxis&apos;]) ??
              PanAxis.free,
          boundaryMargin:
              CustomArgumentDecoders.edgeInsets(source, [&apos;boundaryMargin&apos;]) ??
                  EdgeInsets.zero,
          constrained: source.v&amp;lt;bool&amp;gt;([&apos;constrained&apos;]) ?? true,
          maxScale: source.v&amp;lt;double&amp;gt;([&apos;maxScale&apos;]) ?? 2.5,
          minScale: source.v&amp;lt;double&amp;gt;([&apos;minScale&apos;]) ?? 0.8,
          interactionEndFrictionCoefficient:
              source.v&amp;lt;double&amp;gt;([&apos;interactionEndFrictionCoefficient&apos;]) ??
                  0.0000135,
          panEnabled: source.v&amp;lt;bool&amp;gt;([&apos;panEnabled&apos;]) ?? true,
          scaleEnabled: source.v&amp;lt;bool&amp;gt;([&apos;scaleEnabled&apos;]) ?? true,
          scaleFactor: source.v&amp;lt;double&amp;gt;([&apos;scaleFactor&apos;]) ?? 0.8,
          alignment:
              ArgumentDecoders.alignment(source, [&apos;alignment&apos;]) as Alignment?,
          trackpadScrollCausesScale:
              source.v&amp;lt;bool&amp;gt;([&apos;trackpadScrollCausesScale&apos;]) ?? false,
          child: source.child([&apos;child&apos;]),
        );
      },
    };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example we are using the &lt;a href=&quot;https://m3.material.io/&quot;&gt;Material&lt;/a&gt; widgets but this could be the &lt;a href=&quot;https://pub.dev/packages/fluent_ui&quot;&gt;fluent_ui&lt;/a&gt; or &lt;a href=&quot;https://pub.dev/packages/macos_ui&quot;&gt;macos_ui&lt;/a&gt; package set of components or even your custom design system.&lt;/p&gt;
&lt;p&gt;Now that we have defined the core of rfw we can start to add the logic and UI for our app.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The rfw package includes material and core directly so you can simply import it directly and not need to define like this.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Connecting to the server&lt;/h3&gt;
&lt;p&gt;Create and update the following file located at &lt;code&gt;app/lib/main.dart&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;network.dart&apos;;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter SSR Example&apos;,
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const NetworkExample(),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next create and update the following file located at &lt;code&gt;app/lib/network.dart&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:http/http.dart&apos; as http;

import &apos;package:rfw/rfw.dart&apos;;

import &apos;rfw/material.dart&apos; as m;
import &apos;rfw/core.dart&apos; as c;

class NetworkExample extends StatefulWidget {
  const NetworkExample({super.key});

  @override
  State&amp;lt;NetworkExample&amp;gt; createState() =&amp;gt; _NetworkExampleState();
}

class _NetworkExampleState extends State&amp;lt;NetworkExample&amp;gt; {
  final Runtime _runtime = Runtime();
  final DynamicContent _data = DynamicContent();
  bool loaded = false;
  int count = 0;
  final route = Uri.parse(&apos;http://localhost:8080/&apos;);

  @override
  void initState() {
    super.initState();
    _update();
  }

  @override
  void reassemble() {
    super.reassemble();
    _update();
  }

  static const coreName = LibraryName([&apos;widgets&apos;]);
  static const materialName = LibraryName([&apos;material&apos;]);
  static const remoteName = LibraryName([&apos;remote&apos;]);

  void _update() async {
    _runtime.update(coreName, c.createCoreWidgets());
    _runtime.update(materialName, m.createMaterialWidgets());
    await fetchWidget();
    if (mounted) setState(() =&amp;gt; loaded = true);
  }

  Future&amp;lt;void&amp;gt; fetchWidget() async {
    final res = await http.get(route, headers: {
      &apos;COUNTER_VALUE&apos;: count.toString(),
    });
    if (res.statusCode == 200) {
      count = int.tryParse(res.headers[&apos;counter_value&apos;].toString()) ?? count;
      _data.update(&apos;counter&apos;, &amp;lt;String, Object&amp;gt;{&apos;value&apos;: &apos;$count&apos;});
      _runtime.update(remoteName, decodeLibraryBlob(res.bodyBytes));
    }
  }

  void onEvent(String name, DynamicMap arguments) async {
    debugPrint(&apos;user triggered event &amp;quot;$name&amp;quot; with data: $arguments&apos;);
    if (name == &apos;click&apos;) {
      final res = await http.post(route, headers: {
        &apos;COUNTER_VALUE&apos;: count.toString(),
      });
      if (res.statusCode == 200) {
        count = int.tryParse(res.headers[&apos;counter_value&apos;].toString()) ?? count;
        _data.update(&apos;counter&apos;, &amp;lt;String, Object&amp;gt;{&apos;value&apos;: &apos;$count&apos;});
        _runtime.update(remoteName, decodeLibraryBlob(res.bodyBytes));
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;
    _data.update(&apos;colorScheme&apos;, &amp;lt;String, Object&amp;gt;{
      &apos;inversePrimary&apos;: colors.inversePrimary.value,
      &apos;inverseSurface&apos;: colors.inverseSurface.value,
      &apos;onInverseSurface&apos;: colors.onInverseSurface.value,
      &apos;primary&apos;: colors.primary.value,
      &apos;onPrimary&apos;: colors.onPrimary.value,
      &apos;primaryContainer&apos;: colors.primaryContainer.value,
      &apos;onPrimaryContainer&apos;: colors.onPrimaryContainer.value,
      &apos;secondary&apos;: colors.secondary.value,
      &apos;onSecondary&apos;: colors.onSecondary.value,
      &apos;secondaryContainer&apos;: colors.secondaryContainer.value,
      &apos;onSecondaryContainer&apos;: colors.onSecondaryContainer.value,
      &apos;tertiary&apos;: colors.tertiary.value,
      &apos;onTertiary&apos;: colors.onTertiary.value,
      &apos;tertiaryContainer&apos;: colors.tertiaryContainer.value,
      &apos;onTertiaryContainer&apos;: colors.onTertiaryContainer.value,
      &apos;error&apos;: colors.error.value,
      &apos;onError&apos;: colors.onError.value,
      &apos;errorContainer&apos;: colors.errorContainer.value,
      &apos;onErrorContainer&apos;: colors.onErrorContainer.value,
      &apos;background&apos;: colors.background.value,
      &apos;onBackground&apos;: colors.onBackground.value,
      &apos;surface&apos;: colors.surface.value,
      &apos;onSurface&apos;: colors.onSurface.value,
      &apos;outline&apos;: colors.outline.value,
      &apos;outlineVariant&apos;: colors.outlineVariant.value,
      &apos;scrim&apos;: colors.scrim.value,
      &apos;shadow&apos;: colors.shadow.value,
    });
    if (!loaded) {
      return const Center(child: CircularProgressIndicator());
    }
    const root = FullyQualifiedWidgetName(remoteName, &apos;root&apos;);
    return Container(
      color: Theme.of(context).scaffoldBackgroundColor,
      child: RemoteWidget(
        runtime: _runtime,
        data: _data,
        widget: root,
        onEvent: onEvent,
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a lot going on here but simply we are connecting to our server and defining the local widgets we created and creating a local counter state that is used to send to the server and update per the response.&lt;/p&gt;
&lt;p&gt;Because the &lt;code&gt;DynamicContent _data&lt;/code&gt; can be updated every frame we are setting the local colors from the apps current theme int he build method:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;_data.update(&apos;colorScheme&apos;, &amp;lt;String, Object&amp;gt;{
      &apos;inversePrimary&apos;: colors.inversePrimary.value,
      &apos;inverseSurface&apos;: colors.inverseSurface.value,
      &apos;onInverseSurface&apos;: colors.onInverseSurface.value,
      &apos;primary&apos;: colors.primary.value,
      &apos;onPrimary&apos;: colors.onPrimary.value,
      &apos;primaryContainer&apos;: colors.primaryContainer.value,
      &apos;onPrimaryContainer&apos;: colors.onPrimaryContainer.value,
      &apos;secondary&apos;: colors.secondary.value,
      &apos;onSecondary&apos;: colors.onSecondary.value,
      &apos;secondaryContainer&apos;: colors.secondaryContainer.value,
      &apos;onSecondaryContainer&apos;: colors.onSecondaryContainer.value,
      &apos;tertiary&apos;: colors.tertiary.value,
      &apos;onTertiary&apos;: colors.onTertiary.value,
      &apos;tertiaryContainer&apos;: colors.tertiaryContainer.value,
      &apos;onTertiaryContainer&apos;: colors.onTertiaryContainer.value,
      &apos;error&apos;: colors.error.value,
      &apos;onError&apos;: colors.onError.value,
      &apos;errorContainer&apos;: colors.errorContainer.value,
      &apos;onErrorContainer&apos;: colors.onErrorContainer.value,
      &apos;background&apos;: colors.background.value,
      &apos;onBackground&apos;: colors.onBackground.value,
      &apos;surface&apos;: colors.surface.value,
      &apos;onSurface&apos;: colors.onSurface.value,
      &apos;outline&apos;: colors.outline.value,
      &apos;outlineVariant&apos;: colors.outlineVariant.value,
      &apos;scrim&apos;: colors.scrim.value,
      &apos;shadow&apos;: colors.shadow.value,
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we first load the UI we want to make a &lt;code&gt;GET&lt;/code&gt; request to get the latest from the server or fallback to the latest in cache (or even &lt;code&gt;rootBundle&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;Future&amp;lt;void&amp;gt; fetchWidget() async {
    final res = await http.get(route, headers: {
      &apos;COUNTER_VALUE&apos;: count.toString(),
    });
    if (res.statusCode == 200) {
      count = int.tryParse(res.headers[&apos;counter_value&apos;].toString()) ?? count;
      _data.update(&apos;counter&apos;, &amp;lt;String, Object&amp;gt;{&apos;value&apos;: &apos;$count&apos;});
      _runtime.update(remoteName, decodeLibraryBlob(res.bodyBytes));
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Setting and reading the headers will allow us to send state to the server and respond on updates.&lt;/p&gt;
&lt;p&gt;We can also respond to events in the UI and trigger requests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;void onEvent(String name, DynamicMap arguments) async {
    debugPrint(&apos;user triggered event &amp;quot;$name&amp;quot; with data: $arguments&apos;);
    if (name == &apos;click&apos;) {
      final res = await http.post(route, headers: {
        &apos;COUNTER_VALUE&apos;: count.toString(),
      });
      if (res.statusCode == 200) {
        count = int.tryParse(res.headers[&apos;counter_value&apos;].toString()) ?? count;
        _data.update(&apos;counter&apos;, &amp;lt;String, Object&amp;gt;{&apos;value&apos;: &apos;$count&apos;});
        _runtime.update(remoteName, decodeLibraryBlob(res.bodyBytes));
      }
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are using the response of the &lt;code&gt;POST&lt;/code&gt; request to update the UI but we could also call the &lt;code&gt;fetchWidget&lt;/code&gt; method again to get the latest and use the headers to update the data.&lt;/p&gt;
&lt;p&gt;To run the application simply run using &lt;code&gt;flutter run&lt;/code&gt; and make sure if you use MacOS desktop target to set the correct network permissions.&lt;/p&gt;
&lt;p&gt;If all goes well you should see the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/r_1_phzhlfqsnj.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;This will trigger network requests for each button press and the UI will reflect the logic done on the server.&lt;/p&gt;
&lt;p&gt;You could also call a database and update the UI based on the response.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There is a lot more we can do with this example but after doing a deep dive on the format I thought it would be useful for others to understand and see some examples.&lt;/p&gt;
&lt;p&gt;The final code can be found &lt;a href=&quot;https://github.com/rodydavis/flutter_ssr&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you have any questions reach out to me on &lt;a href=&quot;https://twitter.com/rodydavis&quot;&gt;Twitter&lt;/a&gt; or &lt;a href=&quot;https://github.com/rodydavis&quot;&gt;Github&lt;/a&gt;!&lt;/p&gt;
</content:encoded></item><item><title>Deep Linking for Flutter Web</title><link>https://rodydavis.com/flutter/web-deep-linking/</link><guid isPermaLink="true">https://rodydavis.com/flutter/web-deep-linking/</guid><description>This tutorial demonstrates how to implement deep linking, protected routes, and custom transitions in a Flutter web application.</description><pubDate>Sun, 19 Jan 2025 07:56:03 GMT</pubDate><content:encoded>&lt;h1&gt;Deep Linking for Flutter Web&lt;/h1&gt;
&lt;p&gt;In this article I will show you how to have proper URL navigation for your application. Open links to specific pages, protected routes and custom transitions.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/flutter_deep_linking&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/flutter_deep_linking/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;Create a new flutter project called “flutter_deep_linking”&lt;/p&gt;
&lt;p&gt;Open that folder up in VSCode.&lt;/p&gt;
&lt;p&gt;Update your “pubspec.yaml” with the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_1_qbot26kikl.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Step 1&lt;/h2&gt;
&lt;p&gt;Create a file at “lib/ui/home/screen.dart” and add the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_2_8o7bk1h6ep.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Update your “lib/main.dart” with the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_3_u638b3ccuy.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Run your application and you should see the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_4_fk5zfnixom.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Step 2&lt;/h2&gt;
&lt;p&gt;Now we need to grab the url the user enters into the address bar.&lt;/p&gt;
&lt;p&gt;Create a folder at this location “lib/plugins/navigator”&lt;/p&gt;
&lt;p&gt;Create a file inside named: “web.dart” with the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_5_r37mgvwx5q.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Create a file inside named: “unsupported.dart” with the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_6_xogk4ly3ze.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Create a file inside named: “navigator.dart” with the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_7_atfvh3z2tt.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now go back to your “lib/main.dart” file and add the navigator:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_8_4esmqtror6.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It’s important to import the navigator as shown as this will have the conditional import for web compiling.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you run the app now nothing should change.&lt;/p&gt;
&lt;h2&gt;Step 3&lt;/h2&gt;
&lt;p&gt;Now let’s add the proper routing.&lt;/p&gt;
&lt;p&gt;Create a new file “lib/ui/router.dart” and add the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_9_xae1ebw89x.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Also update “lib/main.dart” with the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_10_ijxwpy9f4h.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Notice how we removed the “home” field for MaterialApp. This is because the router will handle everything. By default we will go home on “/”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Step 4&lt;/h2&gt;
&lt;p&gt;Now let’s add multiple screens to put this to the test! Add the following folders and files.&lt;/p&gt;
&lt;p&gt;Create a file “lib/ui/account/screen.dart” and add the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_11_o9tev9myvc.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Create a file “lib/ui/settings/screen.dart” and add the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_12_8gbj5hxdmq.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Create a file “lib/ui/about/screen.dart” and add the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_13_aojf2nh3fj.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Add the following to “lib/ui/router.dart”:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_14_9p3q4k5nqt.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now when you navigate to /about, /account and /settings you will go to the new pages!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_15_lahb8r2wyw.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Step 5&lt;/h2&gt;
&lt;p&gt;Now let’s tie into the browser navigation buttons! Update “lib/ui/home/screen.dart” with the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_16_6ccpd3agpc.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now when you run the application and click on the settings icon it will launch the new screen as expected. But if you click your browsers back button it will go back to the home screen!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_17_rovfeoa2t1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_18_75rxz9598q.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Step 6&lt;/h2&gt;
&lt;p&gt;These urls are great but what if you want to pass data such as an ID that is not known ahead of time? No worries!&lt;/p&gt;
&lt;p&gt;Update “lib/ui/account/screen.dart” with the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_19_qyhv82wxfe.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Let’s update our “lib/ui/router.dart” with the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_20_i6mmwwv4bg.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now when you run your application and navigate to “/account/40” you will see the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/deep_21_ytsnzw349i.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Dynamic routes work great for Flutter web, you just need to know what to tweak! This package uses a forked version of fluro for some fixes I added but once the PRs is merged you can just use the regular package. Let me know what you think below and if there is a better way I am not seeing!&lt;/p&gt;
&lt;p&gt;Here is the final code: &lt;a href=&quot;https://github.com/rodydavis/flutter_deep_linking&quot;&gt;https://github.com/rodydavis/flutter_deep_linking&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>How To Send Push Notifications on Flutter Web (FCM)</title><link>https://rodydavis.com/flutter/web-push-notifications/</link><guid isPermaLink="true">https://rodydavis.com/flutter/web-push-notifications/</guid><description>Implement Firebase Cloud Messaging (FCM) on Flutter Web by registering a service worker and initializing the Firebase app with your project credentials.</description><pubDate>Mon, 20 Jan 2025 01:11:12 GMT</pubDate><content:encoded>&lt;h1&gt;How To Send Push Notifications on Flutter Web (FCM)&lt;/h1&gt;
&lt;p&gt;If you are using Firebase then you are probably familiar with Firebase Cloud Messaging. The setup on Flutter web is very different than mobile and other plugins you are probably used to.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/p_1_sml0ik8i1v.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Setting Up&lt;/h2&gt;
&lt;p&gt;Open your web/index.html and look for the following script. If you do not have one you can add it now in the tag.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;script&amp;gt;
if (&amp;quot;serviceWorker&amp;quot; in navigator) {
  window.addEventListener(&amp;quot;load&amp;quot;, function () {
    navigator.serviceWorker.register(&amp;quot;/flutter_service_worker.js&amp;quot;);
  });
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to modify it to support the FCM service worker. The important thing we need to do is comment out the flutter_service_worker.js so that we will not get 404 errors when registering the FCM service worker.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;script&amp;gt;
if (&amp;quot;serviceWorker&amp;quot; in navigator) {
  window.addEventListener(&amp;quot;load&amp;quot;, function () {
    // navigator.serviceWorker.register(&amp;quot;/flutter_service_worker.js&amp;quot;);
    navigator.serviceWorker.register(&amp;quot;/firebase-messaging-sw.js&amp;quot;);
  });
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now create a new file called firebase-messaging-sw.js in the web folder with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;importScripts(&amp;quot;https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js&amp;quot;);
importScripts(&amp;quot;https://www.gstatic.com/firebasejs/7.5.0/firebase-messaging.js&amp;quot;);
firebase.initializeApp({
    apiKey: &amp;quot;API_KEY&amp;quot;,
    authDomain: &amp;quot;AUTH_DOMAIN&amp;quot;,
    databaseURL: &amp;quot;DATABASE_URL&amp;quot;,
    projectId: &amp;quot;PROJECT_ID&amp;quot;,
    storageBucket: &amp;quot;STORAGE_BUCKET&amp;quot;,
    messagingSenderId: &amp;quot;MESSAGING_SENDER_ID&amp;quot;,
    appId: &amp;quot;APP_ID&amp;quot;,
    measurementId: &amp;quot;MEASUREMENT_ID&amp;quot;
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function (payload) {
    const promiseChain = clients
        .matchAll({
            type: &amp;quot;window&amp;quot;,
            includeUncontrolled: true
        })
        .then(windowClients =&amp;gt; {
            for (let i = 0; i &amp;lt; windowClients.length; i++) {
                const windowClient = windowClients[i];
                windowClient.postMessage(payload);
            }
        })
        .then(() =&amp;gt; {
            return registration.showNotification(&amp;quot;New Message&amp;quot;);
        });
    return promiseChain;
});
self.addEventListener(&apos;notificationclick&apos;, function (event) {
    console.log(&apos;notification received: &apos;, event)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to replace the config keys with your firebase keys.&lt;/p&gt;
&lt;h2&gt;Helper Methods&lt;/h2&gt;
&lt;p&gt;Create a new dart file wherever you like named firebase_messaging.dart with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:async&apos;;
import &apos;package:firebase/firebase.dart&apos; as firebase;

class FBMessaging {
  FBMessaging._();
  static FBMessaging _instance = FBMessaging._();
  static FBMessaging get instance =&amp;gt; _instance;
  firebase.Messaging _mc;
  String _token;

  final _controller = StreamController&amp;lt;Map&amp;lt;String, dynamic&amp;gt;&amp;gt;.broadcast();
  Stream&amp;lt;Map&amp;lt;String, dynamic&amp;gt;&amp;gt; get stream =&amp;gt; _controller.stream;

  void close() {
    _controller?.close();
  }

  Future&amp;lt;void&amp;gt; init() async {
    _mc = firebase.messaging();
    _mc.usePublicVapidKey(&apos;FCM_SERVER_KEY&apos;);
    _mc.onMessage.listen((event) {
      _controller.add(event?.data);
    });
  }

  Future requestPermission() {
    return _mc.requestPermission();
  }

  Future&amp;lt;String&amp;gt; getToken([bool force = false]) async {
    if (force || _token == null) {
      await requestPermission();
      _token = await _mc.getToken();
    }
    return _token;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a button in the app that will be used to request permissions. While it is possible to request for permission when the app launches this is usually bad practice as the user is unlikely to accept and there is no trust built yet. You can request permissions with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final _messaging = FBMessaging.instance;
_messaging.requestPermission().then((_) async {
  final _token = await _messaging.getToken();
  print(&apos;Token: $_token&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can listen to messages with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final _messaging = FBMessaging.instance;
_messaging.stream.listen((event) {
  print(&apos;New Message: ${event}&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;p&gt;Now when you run your application and request permissions you will get a token back. With this token you can open the firebase console and sent a test message to the token.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/p_2_cab21gsn50.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Now you can send push notifications to Flutter apps! You still need to use conditional imports to support the mobile side as well but stay tuned for an example with that. Let me know your questions and any feedback you may have.&lt;/p&gt;
</content:encoded></item><item><title>How to build a Flutter app on Xcode Cloud</title><link>https://rodydavis.com/flutter/xcode-cloud/</link><guid isPermaLink="true">https://rodydavis.com/flutter/xcode-cloud/</guid><description>This article explains how to set up Xcode Cloud for building Flutter applications, including installing dependencies, running Flutter doctor, and building for TestFlight and the App Store.</description><pubDate>Mon, 20 Jan 2025 00:38:40 GMT</pubDate><content:encoded>&lt;h1&gt;How to build a Flutter app on Xcode Cloud&lt;/h1&gt;
&lt;p&gt;In this article we are going to go over how to setup &lt;a href=&quot;https://developer.apple.com/xcode-cloud/&quot;&gt;Xcode Cloud&lt;/a&gt; to build your &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt; application for &lt;a href=&quot;https://developer.apple.com/testflight/&quot;&gt;TestFlight&lt;/a&gt; and the &lt;a href=&quot;https://developer.apple.com/app-store/&quot;&gt;AppStore&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Step 1&lt;/h2&gt;
&lt;p&gt;Before we begin Flutter needs to be installed, and you can check by running the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;flutter doctor -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After it is installed we can run the following command to create and open our Flutter project (skip down to step 2 if adding to an existing app).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir flutter_ci_example
cd flutter_ci_example
flutter create .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need more help with creating the first project you can check out my previous blog post &lt;a href=&quot;https://rodydavis.com/posts/first-flutter-project/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After the project is created open it in your favorite code editor.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;code .
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2&lt;/h2&gt;
&lt;p&gt;The generated files should look like the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/x_1_qw8btmibvc.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Create a new file at &lt;code&gt;ios/ci_scripts/ci_post_install.sh&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;#!/bin/sh

# Install CocoaPods using Homebrew.
brew install cocoapods

# Install Flutter
brew install --cask flutter

# Run Flutter doctor
flutter doctor

# Get packages
flutter packages get

# Update generated files
flutter pub run build_runner build

# Build ios app
flutter build ios --no-codesign
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a file Xcode Cloud needs to run after the project is downloaded. We need to install &lt;a href=&quot;https://cocoapods.org/&quot;&gt;cocoapods&lt;/a&gt; for any plugins we are using and Flutter to prebuild our application.&lt;/p&gt;
&lt;p&gt;Then run the following command which will make the script executable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;chmod +x ios/ci_scripts/ci_post_clone.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3&lt;/h2&gt;
&lt;p&gt;Open up the iOS project in Xcode by right clicking on the iOS folder and selecting &amp;quot;Open in Xcode&amp;quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/x_2_i34bfwz1u0.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can also open the project by double clicking on the &lt;code&gt;ios/Runner.xcworkspace&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/x_3_80qu3qrdlp.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Make sure you have the latest version of Xcode Cloud install and that you have &lt;a href=&quot;https://developer.apple.com/xcode-cloud/beta/&quot;&gt;access to the beta&lt;/a&gt;. Create a new workflow by the menu &lt;code&gt;Product &amp;gt; Xcode Cloud &amp;gt; Create Workflow&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/x_4_xsrdzbddjh.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Follow the flow to add the project and choose which type of build you want.&lt;/p&gt;
&lt;p&gt;Make sure to remove MacOS as a target in the workflow by selecting &lt;code&gt;Archive - MacOS&lt;/code&gt; and the delete icon on the top right.&lt;/p&gt;
&lt;p&gt;If you want to build and release the MacOS app you will need to do that with another script in the macos folder and a workflow in that Xcode workspace.&lt;/p&gt;
&lt;p&gt;You can create the file &lt;code&gt;macos/ci_scripts/ci_post_clone.sh&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;#!/bin/sh

# Install CocoaPods using Homebrew.
brew install cocoapods

# Install Flutter
brew install --cask flutter

# Run Flutter doctor
flutter doctor

# Enable macos
flutter config --enable-macos-desktop

# Get packages
flutter packages get

# Update generated files
flutter pub run build_runner build

# Build ios app
flutter build ios --no-codesign
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If all goes well it will look like the following after a successful build:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/x_5_zgz4x31cbp.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Flutter makes it ease to build and deploy to multiple platforms and Xcode Cloud takes care of the signing for Apple platforms.&lt;/p&gt;
&lt;p&gt;You can learn more about cd and flutter &lt;a href=&quot;https://docs.flutter.dev/deployment/cd&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Dynamic Themes with CodeMirror</title><link>https://rodydavis.com/lit/codemirror-dynamic-theme/</link><guid isPermaLink="true">https://rodydavis.com/lit/codemirror-dynamic-theme/</guid><description>Create a dynamic code window with CodeMirror and Material Design theming using Lit, Vite, and TypeScript.</description><pubDate>Sun, 19 Jan 2025 23:52:54 GMT</pubDate><content:encoded>&lt;h1&gt;Dynamic Themes with CodeMirror&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to set up a &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; web component and use it to create a a code window that uses &lt;a href=&quot;https://codemirror.net/&quot;&gt;CodeMirror&lt;/a&gt; and apply a dynamic theme with &lt;a href=&quot;https://material.io/&quot;&gt;Material Design&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/codemirror-dynamic-theme&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/codemirror-dynamic-theme/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Node &amp;gt;= 16&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by navigating in terminal to the location of the project and run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init @vitejs/app --template lit-ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enter a project name &lt;code&gt;codemirror-dynamic-theme&lt;/code&gt; and now open the project in vscode and install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd codemirror-dynamic-theme
npm i lit codemirror @material/material-color-utilities
npm i -D @types/node @types/codemirror
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;vite.config.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from &amp;quot;vite&amp;quot;;
import { resolve } from &amp;quot;path&amp;quot;;

export default defineConfig({
  base: &amp;quot;/codemirror-dynamic-theme/&amp;quot;,
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, &amp;quot;index.html&amp;quot;),
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Template&lt;/h2&gt;
&lt;p&gt;Open up the &lt;code&gt;index.html&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/src/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;CodeMirror Dynamic Theme&amp;lt;/title&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/code-window.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        margin: 0;
        padding: 0;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;code-window&amp;gt; &amp;lt;/code-window&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Web Component&lt;/h2&gt;
&lt;p&gt;Before we update our component we need to rename &lt;code&gt;my-element.ts&lt;/code&gt; to &lt;code&gt;code-window.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open up &lt;code&gt;code-window.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, css, LitElement, unsafeCSS } from &amp;quot;lit&amp;quot;;
import { customElement, property } from &amp;quot;lit/decorators.js&amp;quot;;
import {
  applyTheme,
  argbFromHex,
  hexFromArgb,
  themeFromSourceColor,
} from &amp;quot;@material/material-color-utilities&amp;quot;;
import CodeMirror from &amp;quot;codemirror&amp;quot;;
import codemirrorStyles from &amp;quot;codemirror/lib/codemirror.css&amp;quot;;

@customElement(&amp;quot;code-window&amp;quot;)
export class CodeWindow extends LitElement {
  static styles = css`
    ${unsafeCSS(codemirrorStyles)}

    main {
      width: 100vw;
      height: 100vh;
      background-color: var(--md-sys-color-background);
      color: var(--md-sys-color-on-background);
      --header-height: 48px;
      --input-size: 32px;
    }

    .toolbar {
      height: var(--header-height);
      background-color: var(--md-sys-color-primary-container);
      color: var(--md-sys-color-on-primary-container);
      display: flex;
      align-items: center;
    }

    .actions &amp;gt; * {
      margin-left: 4px;
      margin-right: 4px;
    }

    .toolbar .title {
      font-family: sans-serif;
      font-size: 18px;
      padding-left: 4px;
    }

    .toolbar .actions {
      display: flex;
      align-items: center;
    }

    .toolbar a {
      padding: 0;
      margin: 0;
      padding-left: 8px;
      padding-right: 8px;
      display: flex;
      align-items: center;
      cursor: pointer;
    }

    input[type=&amp;quot;color&amp;quot;] {
      width: calc(var(--input-size) * 2);
      height: var(--input-size);
      outline: none;
      border: none;
      border-radius: 50%;
      background-color: var(--md-sys-color-primary-container);
    }
    input[type=&amp;quot;color&amp;quot;]::-webkit-color-swatch-wrapper {
      padding: 0;
    }
    input[type=&amp;quot;color&amp;quot;]::-webkit-color-swatch {
      border: none;
      border-radius: var(--input-size);
      border: var(--md-sys-color-outline) solid 1px;
    }

    button {
      border: none;
      border-radius: 4px;
      padding: 8px;
    }

    .tertiary {
      background-color: var(--md-sys-color-tertiary);
      color: var(--md-sys-color-on-tertiary);
    }

    .secondary {
      background-color: var(--md-sys-color-secondary);
      color: var(--md-sys-color-on-secondary);
    }

    .spacer {
      flex: 1;
    }

    .editor {
      height: calc(100% - var(--header-height));
      width: 100%;
    }
  `;

  @property() value = [
    `import {html, css, LitElement} from &apos;lit&apos;;`,
    `import {customElement, property} from &apos;lit/decorators.js&apos;;`,
    ``,
    `@customElement(&apos;simple-greeting&apos;)`,
    `export class SimpleGreeting extends LitElement {`,
    `  static styles = css\`p { color: blue }\`;`,
    ``,
    `  @property()`,
    `  name = &apos;Somebody&apos;;`,
    ``,
    `  render() {`,
    `    return html\`&amp;lt;p&amp;gt;Hello, \${this.name}!&amp;lt;/p&amp;gt;\`;`,
    `  }`,
    `}`,
  ].join(&amp;quot;\n&amp;quot;);

  @property() color = &amp;quot;#6750A4&amp;quot;;
  @property({ type: Boolean }) dark = window.matchMedia(
    &amp;quot;(prefers-color-scheme: dark)&amp;quot;
  ).matches;

  render() {
    return html`&amp;lt;main&amp;gt;
      &amp;lt;header class=&amp;quot;toolbar&amp;quot;&amp;gt;
        &amp;lt;div class=&amp;quot;title&amp;quot;&amp;gt;${document.title}&amp;lt;/div&amp;gt;
        &amp;lt;div class=&amp;quot;spacer&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class=&amp;quot;actions&amp;quot;&amp;gt;
          &amp;lt;button class=&amp;quot;secondary&amp;quot; @click=${this.toggleDark.bind(this)}&amp;gt;
            ${this.dark ? &amp;quot;Light&amp;quot; : &amp;quot;Dark&amp;quot;}
          &amp;lt;/button&amp;gt;
          &amp;lt;button class=&amp;quot;tertiary&amp;quot; @click=${this.randomColor.bind(this)}&amp;gt;
            Random
          &amp;lt;/button&amp;gt;
          &amp;lt;input
            type=&amp;quot;color&amp;quot;
            .value=${this.color}
            @input=${this.onColor.bind(this)}
          /&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/header&amp;gt;
      &amp;lt;div class=&amp;quot;editor&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/main&amp;gt;`;
  }

  firstUpdated() {
    const root = this.shadowRoot!.querySelector(&amp;quot;.editor&amp;quot;) as HTMLElement;
    const editor = CodeMirror(root, {
      value: this.value,
      mode: &amp;quot;javascript&amp;quot;,
      lineNumbers: true,
      lineWrapping: true,
      indentUnit: 4,
      tabSize: 4,
      indentWithTabs: true,
      autofocus: true,
    });
    console.debug(editor);
    editor.setSize(&amp;quot;100%&amp;quot;, `100%`);
    this.updateTheme();
    window
      .matchMedia(&amp;quot;(prefers-color-scheme: dark)&amp;quot;)
      .addEventListener(&amp;quot;change&amp;quot;, (e) =&amp;gt; {
        this.dark = e.matches;
        this.updateTheme();
      });
  }

  private updateTheme() {
   // TODO: Generate Theme
  }

  private setColor(val: string) {
    this.color = val;
    this.updateTheme();
  }

  private onColor(e: Event) {
    const target = e.target as HTMLInputElement;
    this.setColor(target.value);
  }

  private randomColor() {
    const letters = &amp;quot;0123456789ABCDEF&amp;quot;;
    let color = &amp;quot;#&amp;quot;;
    for (let i = 0; i &amp;lt; 6; i++) {
      color += letters[Math.floor(Math.random() * 16)];
    }
    this.setColor(color);
  }

  private toggleDark() {
    this.dark = !this.dark;
    this.updateTheme();
  }

}

declare global {
  interface HTMLElementTagNameMap {
    &amp;quot;code-window&amp;quot;: CodeWindow;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are setting up some of the editor basics to load in the styles needed for the basic layout.&lt;/p&gt;
&lt;p&gt;There are also few methods that handle updating of properties on the element such as &lt;code&gt;toggleDark&lt;/code&gt; and &lt;code&gt;setColor&lt;/code&gt;. When you run the application you should see the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/cm_1_vrf88lj26h.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;It doesn&apos;t look great yet, but now we can add a CodeMirror theme to import. Create a file &lt;code&gt;src/theme.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;import { css } from &amp;quot;lit&amp;quot;;

export const codeMirrorTheme = css`
  .CodeMirror {
    background-color: var(--md-sys-color-background);
    color: var(--md-sys-color-on-background);
  }

  .CodeMirror-gutters {
    background: var(--md-sys-color-surface-variant);
    color: var(--md-sys-color-on-surface-variant);
    border: none;
  }

  .CodeMirror-guttermarker,
  .CodeMirror-guttermarker-subtle,
  .CodeMirror-linenumber {
    color: var(--md-sys-color-on-background);
  }

  .CodeMirror-cursor {
    border-left: 1px solid var(--md-sys-color-primary);
  }
  .cm-fat-cursor .CodeMirror-cursor {
    background-color: var(--md-sys-color-background);
  }
  .cm-animate-fat-cursor {
    background-color: var(--md-sys-color-background);
  }

  div.CodeMirror-selected {
    background: var(--md-sys-color-surface-variant);
  }

  .CodeMirror-focused div.CodeMirror-selected {
    background: var(--md-sys-color-surface-variant);
  }

  .CodeMirror-line::selection,
  .CodeMirror-line &amp;gt; span::selection,
  .CodeMirror-line &amp;gt; span &amp;gt; span::selection {
    background: var(--md-sys-color-surface-variant);
  }

  .CodeMirror-line::-moz-selection,
  .CodeMirror-line &amp;gt; span::-moz-selection,
  .CodeMirror-line &amp;gt; span &amp;gt; span::-moz-selection {
    background: var(--md-sys-color-surface-variant);
  }

  .CodeMirror-activeline-background {
    background: var(--md-sys-color-surface);
  }

  .cm-keyword {
    color: var(--md-custom-color-keyword) !important;
  }

  .cm-operator {
    color: var(--md-custom-color-operator) !important;
  }

  .cm-variable-2 {
    color: var(--md-custom-color-variable-2) !important;
  }

  .cm-variable-3,
  .cm-type {
    color: var(--md-custom-color-variable-3) !important;
  }

  .cm-builtin {
    color: var(--md-custom-color-builtin) !important;
  }

  .cm-atom {
    color: var(--md-custom-color-atom) !important;
  }

  .cm-number {
    color: var(--md-custom-color-number) !important;
  }

  .cm-def {
    color: var(--md-custom-color-def) !important;
  }

  .cm-string {
    color: var(--md-custom-color-string) !important;
  }

  .cm-string-2 {
    color: var(--md-custom-color-string-2) !important;
  }

  .cm-comment {
    color: var(--md-custom-color-comment) !important;
  }

  .cm-variable {
    color: var(--md-custom-color-variable) !important;
  }

  .cm-tag {
    color: var(--md-custom-color-tag) !important;
  }

  .cm-meta {
    color: var(--md-custom-color-meta) !important;
  }

  .cm-attribute {
    color: var(--md-custom-color-attribute) !important;
  }

  .cm-property {
    color: var(--md-custom-color-property) !important;
  }

  .cm-qualifier {
    color: var(--md-custom-color-qualifier) !important;
  }

  .cm-variable-3,
  .cm-type {
    color: var(--md-custom-color-variable-3) !important;
  }

  .cm-error {
    color: var(--md-sys-color-on-error);
    background-color: var(--md-sys-color-error);
  }

  .CodeMirror-matchingbracket {
    text-decoration: underline;
    color: var(--md-sys-color-on-surface);
  }
`;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are defining all the styles as &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties&quot;&gt;CSS Custom Properties&lt;/a&gt; so we can easily update them.&lt;/p&gt;
&lt;p&gt;Now import the theme and apply the styles in the component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { codeMirrorTheme } from &amp;quot;./theme&amp;quot;;

@customElement(&amp;quot;code-window&amp;quot;)
export class CodeWindow extends LitElement {
  static styles = css`
    ${unsafeCSS(codemirrorStyles)}
    ${codeMirrorTheme}
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to implement the &lt;code&gt;updateTheme&lt;/code&gt; method in our element:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;updateTheme() {
    const source = this.color;
    const dark = this.dark;
    const target = this.shadowRoot!.querySelector(&amp;quot;main&amp;quot;) as HTMLElement;
    const properties = [
        `--md-custom-color-keyword: #c75779;`,
        `--md-custom-color-operator: #008800;`,
        `--md-custom-color-variable: #90ccff;`,
        `--md-custom-color-variable-2: #dd7700;`,
        `--md-custom-color-variable-3: #3333bb;`,
        `--md-custom-color-variable-3: #decb6b;`,
        `--md-custom-color-builtin: #003388;`,
        `--md-custom-color-atom: #bb4646;`,
        `--md-custom-color-number: #b4c5ff;`,
        `--md-custom-color-def: #82aaff;`,
        `--md-custom-color-string: #ffb4a9;`,
        `--md-custom-color-string-2: #ffb4a9;`,
        `--md-custom-color-comment: #888888;`,
        `--md-custom-color-tag: #000080;`,
        `--md-custom-color-meta: #a9c7ff;`,
        `--md-custom-color-attribute: #008080;`,
        `--md-custom-color-property: #336699;`,
        `--md-custom-color-qualifier: #690;`,
    ];
    const customColors = properties.map((property) =&amp;gt; {
        const [key, value] = property.split(&amp;quot;:&amp;quot;);
        const name = key.trim().replace(/^--md-custom-color-/, &amp;quot;&amp;quot;);
        const color = argbFromHex(value.trim().replace(&amp;quot;;&amp;quot;, &amp;quot;&amp;quot;));
        return {
        name,
        value: color,
        blend: true,
        };
    });
    const theme = themeFromSourceColor(argbFromHex(source), customColors);
    applyTheme(theme, { target, dark });
    for (const custom of theme.customColors) {
        const name = custom.color.name;
        const section = dark ? custom.dark : custom.light;
        target.style.setProperty(
        `--md-custom-color-${name}`,
        hexFromArgb(section.color)
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are using the &lt;a href=&quot;https://github.com/material-foundation/material-color-utilities&quot;&gt;Material Color Utilities&lt;/a&gt; package and generating a theme from a source color. We can take advantage of Custom Colors to blend the values to the theme.&lt;/p&gt;
&lt;p&gt;After the theme is generated we can apply them to the root element and have the custom properties update the editor.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/cm_2_i38ka2mknd.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Changing the source color can update the theme:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/cm_3_h3mcbh0c3s.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Changing the brightness can set the colors as well:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/cm_4_u36e81hjiw.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/cm_5_2xo33gqwdh.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to learn more about building with Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The source for this example can be found &lt;a href=&quot;https://github.com/rodydavis/codemirror-dynamic-theme&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you want to check out dynamic themes for VSCode I created an extension &lt;a href=&quot;https://github.com/rodydavis/vscode-dynamic-theme&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Draggable DOM with Lit</title><link>https://rodydavis.com/lit/draggable-dom/</link><guid isPermaLink="true">https://rodydavis.com/lit/draggable-dom/</guid><description>Create an interactive DOM with CSS transforms using Lit, a web component framework, with a detailed setup guide and example code.</description><pubDate>Sun, 19 Jan 2025 08:19:15 GMT</pubDate><content:encoded>&lt;h1&gt;Draggable DOM with Lit&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to set up a &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; web component and use it to create a interactive dom with CSS transforms and slots.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/lit-draggable-dom&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/lit-draggable-dom/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Node &amp;gt;= 16&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by navigating in terminal to the location of the project and run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init @vitejs/app --template lit-ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enter a project name &lt;code&gt;lit-draggable-dom&lt;/code&gt; and now open the project in vscode and install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd lit-draggable-dom
npm i lit
npm i -D @types/node
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;vite.config.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from &amp;quot;vite&amp;quot;;
import { resolve } from &amp;quot;path&amp;quot;;

export default defineConfig({
  base: &amp;quot;/lit-draggable-dom/&amp;quot;,
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, &amp;quot;index.html&amp;quot;),
      },
    },
  },
});

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Template&lt;/h2&gt;
&lt;p&gt;Open up the &lt;code&gt;index.html&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/src/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;Lit Draggable DOM&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100vh;
      }
    &amp;lt;/style&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/draggable-dom.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;draggable-dom&amp;gt;
      &amp;lt;img
        src=&amp;quot;https://lit.dev/images/logo.svg&amp;quot;
        alt=&amp;quot;Lit Logo&amp;quot;
        width=&amp;quot;500&amp;quot;
        height=&amp;quot;333&amp;quot;
        style=&amp;quot;--dx: 59.4909px; --dy: 32.8429px&amp;quot;
      /&amp;gt;
      &amp;lt;svg width=&amp;quot;400&amp;quot; height=&amp;quot;110&amp;quot; style=&amp;quot;--dx: 230.057px; --dy: 33.6257px&amp;quot;&amp;gt;
        &amp;lt;rect
          width=&amp;quot;400&amp;quot;
          height=&amp;quot;100&amp;quot;
          style=&amp;quot;fill: rgb(0, 0, 255); stroke-width: 3; stroke: rgb(0, 0, 0)&amp;quot;
        /&amp;gt;
      &amp;lt;/svg&amp;gt;
      &amp;lt;svg height=&amp;quot;100&amp;quot; width=&amp;quot;100&amp;quot;&amp;gt;
        &amp;lt;circle
          cx=&amp;quot;50&amp;quot;
          cy=&amp;quot;50&amp;quot;
          r=&amp;quot;40&amp;quot;
          stroke=&amp;quot;black&amp;quot;
          stroke-width=&amp;quot;3&amp;quot;
          fill=&amp;quot;red&amp;quot;
        /&amp;gt;
      &amp;lt;/svg&amp;gt;
    &amp;lt;/draggable-dom&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are setting up the &lt;code&gt;lit-element&lt;/code&gt; to have a few slots which can be any valid HTML or SVG Elements.&lt;/p&gt;
&lt;p&gt;It is optional to set the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/--*&quot;&gt;css custom properties&lt;/a&gt; &lt;code&gt;--dx&lt;/code&gt; and &lt;code&gt;--dy&lt;/code&gt; as this is just the initial positions on the canvas.&lt;/p&gt;
&lt;h2&gt;Web Component&lt;/h2&gt;
&lt;p&gt;Before we update our component we need to rename &lt;code&gt;my-element.ts&lt;/code&gt; to &lt;code&gt;draggable-dom.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open up &lt;code&gt;draggable-dom.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, css, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, query } from &amp;quot;lit/decorators.js&amp;quot;;

type DragType = &amp;quot;none&amp;quot; | &amp;quot;canvas&amp;quot; | &amp;quot;element&amp;quot;;
type SupportedNode = HTMLElement | SVGElement;

@customElement(&amp;quot;draggable-dom&amp;quot;)
export class CSSCanvas extends LitElement {
  @query(&amp;quot;main&amp;quot;) root!: HTMLElement;
  @query(&amp;quot;#children&amp;quot;) container!: HTMLElement;
  @query(&amp;quot;canvas&amp;quot;) canvas!: HTMLCanvasElement;
  dragType: DragType = &amp;quot;none&amp;quot;;
  offset: Offset = { x: 0, y: 0 };
  pointerMap: Map&amp;lt;number, PointerData&amp;gt; = new Map();

  static styles = css`
    :host {
      --offset-x: 0;
      --offset-y: 0;
      --grid-background-color: white;
      --grid-color: black;
      --grid-size: 40px;
      --grid-dot-size: 1px;
    }
    main {
      overflow: hidden;
    }
    canvas {
      background-size: var(--grid-size) var(--grid-size);
      background-image: radial-gradient(
        circle,
        var(--grid-color) var(--grid-dot-size),
        var(--grid-background-color) var(--grid-dot-size)
      );
      background-position: var(--offset-x) var(--offset-y);
      z-index: 0;
    }
    .full-size {
      width: 100%;
      height: 100%;
      position: fixed;
    }
    .child {
      --dx: 0px;
      --dy: 0px;
      position: fixed;
      flex-shrink: 1;
      z-index: var(--layer, 0);
      transform: translate(var(--dx), var(--dy));
    }
    @media (prefers-color-scheme: dark) {
      main {
        --grid-background-color: black;
        --grid-color: grey;
      }
    }
  `;

  render() {
    return html`
      &amp;lt;main class=&amp;quot;full-size&amp;quot;&amp;gt;
        &amp;lt;canvas class=&amp;quot;full-size&amp;quot;&amp;gt;&amp;lt;/canvas&amp;gt;
        &amp;lt;div id=&amp;quot;children&amp;quot; class=&amp;quot;full-size&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/main&amp;gt;
    `;
  }
}

interface Offset {
  x: number;
  y: number;
}

interface PointerData {
  id: number;
  startPos: Offset;
  currentPos: Offset;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are just setting up some boilerplate to render a &lt;code&gt;main&lt;/code&gt; element with a &lt;code&gt;canvas&lt;/code&gt; element as a background and the &lt;code&gt;div&lt;/code&gt; element to contain the canvas elements.&lt;/p&gt;
&lt;p&gt;We are also making sure to clip and only render what is visible.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Offset&lt;/code&gt; and &lt;code&gt;PointerData&lt;/code&gt; interfaces will be used for storing the location of each pointer interacting with the screen.&lt;/p&gt;
&lt;p&gt;When the user has dark mode enabled for the system it will change the colors of the canvas grid.&lt;/p&gt;
&lt;p&gt;Now let&apos;s add the slot children to the canvas by adding the following to the class:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async firstUpdated() {
    const items = Array.from(this.childNodes);
    let i = 0;
    for (const node of items) {
        if (node instanceof SVGElement || node instanceof HTMLElement) {
            const child = node as SupportedNode;
            child.classList.add(&amp;quot;child&amp;quot;);
            child.style.setProperty(&amp;quot;--layer&amp;quot;, `${i}`);
            this.container.append(child);
            child.addEventListener(&amp;quot;pointerdown&amp;quot;, (e: any) =&amp;gt; {
                // Pointer Down for Child
            });
            child.addEventListener(&amp;quot;pointermove&amp;quot;, (e: any) =&amp;gt; {
                // Pointer Move for Child
            });
            i++;
        }
    }
    this.requestUpdate();
    this.root.addEventListener(&amp;quot;pointerdown&amp;quot;, (e: any) =&amp;gt; {
        // Pointer Down for Canvas
    });
    this.root.addEventListener(&amp;quot;pointermove&amp;quot;, (e: any) =&amp;gt; {
        // Pointer Move for Canvas
    });
    this.root.addEventListener(&amp;quot;pointerup&amp;quot;, (e: any) =&amp;gt; {
        // Pointer Up for Canvas
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The order of the slots defines what renders on top of each other. For each item in the slot it sets&lt;code&gt;--layer&lt;/code&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/z-index&quot;&gt;&lt;code&gt;z-index&lt;/code&gt;&lt;/a&gt; to the current index.&lt;/p&gt;
&lt;p&gt;Currently nothing is happening when we interact with the elements but things should be rendering.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/dom_1_8lmqmje55f.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now let&apos;s add the event handlers for the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events&quot;&gt;pointer events&lt;/a&gt; by appending the following to the class:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;handleDown(event: PointerEvent, type: DragType) {
    if (this.dragType === &amp;quot;none&amp;quot;) {
        event.preventDefault();
        this.dragType = type;
        (event.target as Element).setPointerCapture(event.pointerId);
        this.pointerMap.set(event.pointerId, {
            id: event.pointerId,
            startPos: { x: event.clientX, y: event.clientY },
            currentPos: { x: event.clientX, y: event.clientY },
        });
    }
}

handleMove(
    event: PointerEvent,
    type: DragType,
    onMove: (delta: Offset) =&amp;gt; void
) {
    if (this.dragType === type) {
        event.preventDefault();
        const saved = this.pointerMap.get(event.pointerId)!;
        const current = { ...saved.currentPos };
        saved.currentPos = { x: event.clientX, y: event.clientY };
        const delta = {
            x: saved.currentPos.x - current.x,
            y: saved.currentPos.y - current.y,
        };
        onMove(delta);
    }
}

handleUp(event: PointerEvent) {
    this.dragType = &amp;quot;none&amp;quot;;
    (event.target as Element).releasePointerCapture(event.pointerId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For each event we want to check if the current event &lt;code&gt;canvas&lt;/code&gt; or &lt;code&gt;element&lt;/code&gt; so if we start moving an element it doesn&apos;t move the canvas and vice versa.&lt;/p&gt;
&lt;p&gt;When we have a pointer interact with the screen we will add it to the pointer map (since it can be multi touch) and start tracking the offset.&lt;/p&gt;
&lt;p&gt;The delta is calculated to move the elements but a global offset is used for the canvas background.&lt;/p&gt;
&lt;p&gt;We are setting the pointer capture events so if the mouse is not perfectly on the item it won&apos;t lose tracking.&lt;/p&gt;
&lt;p&gt;Now let&apos;s add methods for moving the canvas and elements by appending the following to the class:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;moveCanvas(delta: Offset) {
    this.offset.x += delta.x;
    this.offset.y += delta.y;
    this.root.style.setProperty(&amp;quot;--offset-x&amp;quot;, `${this.offset.x}px`);
    this.root.style.setProperty(&amp;quot;--offset-y&amp;quot;, `${this.offset.y}px`);
}

moveElement(child: SupportedNode, delta: Offset) {
    const getNumber = (key: &amp;quot;--dx&amp;quot; | &amp;quot;--dy&amp;quot;, fallback: number) =&amp;gt; {
      const saved = child.style.getPropertyValue(key);
      if (saved.length &amp;gt; 0) {
        return parseFloat(saved.replace(&amp;quot;px&amp;quot;, &amp;quot;&amp;quot;));
      }
      return fallback;
    };
    const dx = getNumber(&amp;quot;--dx&amp;quot;, 0) + delta.x;
    const dy = getNumber(&amp;quot;--dy&amp;quot;, 0) + delta.y;
    child.style.transform = `translate(${dx}px, ${dy}px)`;
    child.style.setProperty(&amp;quot;--dx&amp;quot;, `${dx}px`);
    child.style.setProperty(&amp;quot;--dy&amp;quot;, `${dy}px`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the canvas it will set a global offset for the CSS &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/background-position&quot;&gt;&lt;code&gt;background-position&lt;/code&gt;&lt;/a&gt; and update the saved offset.&lt;/p&gt;
&lt;p&gt;For the element we want to transform by the delta so the animation is smooth thanks to hardware acceleration. After the transform it will store the offset as CSS custom properties.&lt;/p&gt;
&lt;p&gt;Now let&apos;s add the event handlers to the canvas and elements by adjusting the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async firstUpdated() {
    const items = Array.from(this.childNodes);
    let i = 0;
    for (const node of items) {
      if (node instanceof SVGElement || node instanceof HTMLElement) {
        const child = node as SupportedNode;
        child.classList.add(&amp;quot;child&amp;quot;);
        child.style.setProperty(&amp;quot;--layer&amp;quot;, `${i}`);
        this.container.append(child);
        child.addEventListener(&amp;quot;pointerdown&amp;quot;, (e: any) =&amp;gt; {
          this.handleDown(e, &amp;quot;element&amp;quot;);
        });
        child.addEventListener(&amp;quot;pointermove&amp;quot;, (e: any) =&amp;gt; {
          this.handleMove(e, &amp;quot;element&amp;quot;, (delta) =&amp;gt; {
            this.moveElement(child, delta);
          });
        });
        i++;
      }
    }
    this.requestUpdate();
    this.root.addEventListener(&amp;quot;pointerdown&amp;quot;, (e: any) =&amp;gt; {
      this.handleDown(e, &amp;quot;canvas&amp;quot;);
    });
    this.root.addEventListener(&amp;quot;pointermove&amp;quot;, (e: any) =&amp;gt; {
      this.handleMove(e, &amp;quot;canvas&amp;quot;, (delta) =&amp;gt; {
        this.moveCanvas(delta);
        for (const node of Array.from(this.container.children)) {
          if (node instanceof SVGElement || node instanceof HTMLElement) {
            this.moveElement(node, delta);
          }
        }
      });
    });
    this.root.addEventListener(&amp;quot;pointerup&amp;quot;, (e: any) =&amp;gt; {
      this.handleUp(e);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything should work as expected now and the final code should look like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, css, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, query } from &amp;quot;lit/decorators.js&amp;quot;;

type DragType = &amp;quot;none&amp;quot; | &amp;quot;canvas&amp;quot; | &amp;quot;element&amp;quot;;
type SupportedNode = HTMLElement | SVGElement;

@customElement(&amp;quot;draggable-dom&amp;quot;)
export class DraggableDOM extends LitElement {
  @query(&amp;quot;main&amp;quot;) root!: HTMLElement;
  @query(&amp;quot;#children&amp;quot;) container!: HTMLElement;
  @query(&amp;quot;canvas&amp;quot;) canvas!: HTMLCanvasElement;
  dragType: DragType = &amp;quot;none&amp;quot;;
  offset: Offset = { x: 0, y: 0 };
  pointerMap: Map&amp;lt;number, PointerData&amp;gt; = new Map();

  static styles = css`
    :host {
      --offset-x: 0;
      --offset-y: 0;
      --grid-background-color: white;
      --grid-color: black;
      --grid-size: 40px;
      --grid-dot-size: 1px;
    }
    main {
      overflow: hidden;
    }
    canvas {
      background-size: var(--grid-size) var(--grid-size);
      background-image: radial-gradient(
        circle,
        var(--grid-color) var(--grid-dot-size),
        var(--grid-background-color) var(--grid-dot-size)
      );
      background-position: var(--offset-x) var(--offset-y);
      z-index: 0;
    }
    .full-size {
      width: 100%;
      height: 100%;
      position: fixed;
    }
    .child {
      --dx: 0px;
      --dy: 0px;
      position: fixed;
      flex-shrink: 1;
      z-index: var(--layer, 0);
      transform: translate(var(--dx), var(--dy));
    }
    @media (prefers-color-scheme: dark) {
      main {
        --grid-background-color: black;
        --grid-color: grey;
      }
    }
  `;

  render() {
    console.log(&amp;quot;render&amp;quot;);
    return html`
      &amp;lt;main class=&amp;quot;full-size&amp;quot;&amp;gt;
        &amp;lt;canvas class=&amp;quot;full-size&amp;quot;&amp;gt;&amp;lt;/canvas&amp;gt;
        &amp;lt;div id=&amp;quot;children&amp;quot; class=&amp;quot;full-size&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/main&amp;gt;
    `;
  }

  handleDown(event: PointerEvent, type: DragType) {
    if (this.dragType === &amp;quot;none&amp;quot;) {
      event.preventDefault();
      this.dragType = type;
      (event.target as Element).setPointerCapture(event.pointerId);
      this.pointerMap.set(event.pointerId, {
        id: event.pointerId,
        startPos: { x: event.clientX, y: event.clientY },
        currentPos: { x: event.clientX, y: event.clientY },
      });
    }
  }

  handleMove(
    event: PointerEvent,
    type: DragType,
    onMove: (delta: Offset) =&amp;gt; void
  ) {
    if (this.dragType === type) {
      event.preventDefault();
      const saved = this.pointerMap.get(event.pointerId)!;
      const current = { ...saved.currentPos };
      saved.currentPos = { x: event.clientX, y: event.clientY };
      const delta = {
        x: saved.currentPos.x - current.x,
        y: saved.currentPos.y - current.y,
      };
      onMove(delta);
    }
  }

  handleUp(event: PointerEvent) {
    this.dragType = &amp;quot;none&amp;quot;;
    (event.target as Element).releasePointerCapture(event.pointerId);
  }

  moveCanvas(delta: Offset) {
    this.offset.x += delta.x;
    this.offset.y += delta.y;
    this.root.style.setProperty(&amp;quot;--offset-x&amp;quot;, `${this.offset.x}px`);
    this.root.style.setProperty(&amp;quot;--offset-y&amp;quot;, `${this.offset.y}px`);
  }

  moveElement(child: SupportedNode, delta: Offset) {
    const getNumber = (key: &amp;quot;--dx&amp;quot; | &amp;quot;--dy&amp;quot;, fallback: number) =&amp;gt; {
      const saved = child.style.getPropertyValue(key);
      if (saved.length &amp;gt; 0) {
        return parseFloat(saved.replace(&amp;quot;px&amp;quot;, &amp;quot;&amp;quot;));
      }
      return fallback;
    };
    const dx = getNumber(&amp;quot;--dx&amp;quot;, 0) + delta.x;
    const dy = getNumber(&amp;quot;--dy&amp;quot;, 0) + delta.y;
    child.style.transform = `translate(${dx}px, ${dy}px)`;
    child.style.setProperty(&amp;quot;--dx&amp;quot;, `${dx}px`);
    child.style.setProperty(&amp;quot;--dy&amp;quot;, `${dy}px`);
  }

  async firstUpdated() {
    const items = Array.from(this.childNodes);
    let i = 0;
    for (const node of items) {
      if (node instanceof SVGElement || node instanceof HTMLElement) {
        const child = node as SupportedNode;
        child.classList.add(&amp;quot;child&amp;quot;);
        child.style.setProperty(&amp;quot;--layer&amp;quot;, `${i}`);
        this.container.append(child);
        child.addEventListener(&amp;quot;pointerdown&amp;quot;, (e: any) =&amp;gt; {
          this.handleDown(e, &amp;quot;element&amp;quot;);
        });
        child.addEventListener(&amp;quot;pointermove&amp;quot;, (e: any) =&amp;gt; {
          this.handleMove(e, &amp;quot;element&amp;quot;, (delta) =&amp;gt; {
            this.moveElement(child, delta);
          });
        });
        child.setAttribute(&amp;quot;draggable&amp;quot;, &amp;quot;false&amp;quot;);
        i++;
      }
    }
    this.requestUpdate();
    this.root.addEventListener(&amp;quot;pointerdown&amp;quot;, (e: any) =&amp;gt; {
      this.handleDown(e, &amp;quot;canvas&amp;quot;);
    });
    this.root.addEventListener(&amp;quot;pointermove&amp;quot;, (e: any) =&amp;gt; {
      this.handleMove(e, &amp;quot;canvas&amp;quot;, (delta) =&amp;gt; {
        this.moveCanvas(delta);
        for (const node of Array.from(this.container.children)) {
          if (node instanceof SVGElement || node instanceof HTMLElement) {
            this.moveElement(node, delta);
          }
        }
      });
    });
    this.root.addEventListener(&amp;quot;touchstart&amp;quot;, function (e) {
      e.preventDefault();
    });
    this.root.addEventListener(&amp;quot;pointerup&amp;quot;, (e: any) =&amp;gt; {
      this.handleUp(e);
    });
  }
}

interface Offset {
  x: number;
  y: number;
}

interface PointerData {
  id: number;
  startPos: Offset;
  currentPos: Offset;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to learn more about building with Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;. There is also an example on the Lit playground &lt;a href=&quot;https://lit.dev/playground/#project=W3sibmFtZSI6ImxpdC1jc3MtY2FudmFzLnRzIiwiY29udGVudCI6ImltcG9ydCB7IGh0bWwsIGNzcywgTGl0RWxlbWVudCB9IGZyb20gXCJsaXRcIjtcbmltcG9ydCB7IGN1c3RvbUVsZW1lbnQsIHF1ZXJ5IH0gZnJvbSBcImxpdC9kZWNvcmF0b3JzLmpzXCI7XG5cbnR5cGUgRHJhZ1R5cGUgPSBcIm5vbmVcIiB8IFwiY2FudmFzXCIgfCBcImVsZW1lbnRcIjtcbnR5cGUgU3VwcG9ydGVkTm9kZSA9IEhUTUxFbGVtZW50IHwgU1ZHRWxlbWVudDtcblxuQGN1c3RvbUVsZW1lbnQoXCJjc3MtY2FudmFzXCIpXG5leHBvcnQgY2xhc3MgQ1NTQ2FudmFzIGV4dGVuZHMgTGl0RWxlbWVudCB7XG4gIEBxdWVyeShcIm1haW5cIikgcm9vdCE6IEhUTUxFbGVtZW50O1xuICBAcXVlcnkoXCIjY2hpbGRyZW5cIikgY29udGFpbmVyITogSFRNTEVsZW1lbnQ7XG4gIEBxdWVyeShcImNhbnZhc1wiKSBjYW52YXMhOiBIVE1MQ2FudmFzRWxlbWVudDtcbiAgZHJhZ1R5cGU6IERyYWdUeXBlID0gXCJub25lXCI7XG4gIG9mZnNldDogT2Zmc2V0ID0geyB4OiAwLCB5OiAwIH07XG4gIHBvaW50ZXJNYXA6IE1hcDxudW1iZXIsIFBvaW50ZXJEYXRhPiA9IG5ldyBNYXAoKTtcblxuICBzdGF0aWMgc3R5bGVzID0gY3NzYFxuICAgIDpob3N0IHtcbiAgICAgIC0tb2Zmc2V0LXg6IDA7XG4gICAgICAtLW9mZnNldC15OiAwO1xuICAgICAgLS1ncmlkLWJhY2tncm91bmQtY29sb3I6IHdoaXRlO1xuICAgICAgLS1ncmlkLWNvbG9yOiBibGFjaztcbiAgICAgIC0tZ3JpZC1zaXplOiA0MHB4O1xuICAgICAgLS1ncmlkLWRvdC1zaXplOiAxcHg7XG4gICAgfVxuICAgIG1haW4ge1xuICAgICAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgICB9XG4gICAgY2FudmFzIHtcbiAgICAgIGJhY2tncm91bmQtc2l6ZTogdmFyKC0tZ3JpZC1zaXplKSB2YXIoLS1ncmlkLXNpemUpO1xuICAgICAgYmFja2dyb3VuZC1pbWFnZTogcmFkaWFsLWdyYWRpZW50KFxuICAgICAgICBjaXJjbGUsXG4gICAgICAgIHZhcigtLWdyaWQtY29sb3IpIHZhcigtLWdyaWQtZG90LXNpemUpLFxuICAgICAgICB2YXIoLS1ncmlkLWJhY2tncm91bmQtY29sb3IpIHZhcigtLWdyaWQtZG90LXNpemUpXG4gICAgICApO1xuICAgICAgYmFja2dyb3VuZC1wb3NpdGlvbjogdmFyKC0tb2Zmc2V0LXgpIHZhcigtLW9mZnNldC15KTtcbiAgICAgIHotaW5kZXg6IDA7XG4gICAgfVxuICAgIC5mdWxsLXNpemUge1xuICAgICAgd2lkdGg6IDEwMCU7XG4gICAgICBoZWlnaHQ6IDEwMCU7XG4gICAgICBwb3NpdGlvbjogZml4ZWQ7XG4gICAgfVxuICAgIC5jaGlsZCB7XG4gICAgICAtLWR4OiAwcHg7XG4gICAgICAtLWR5OiAwcHg7XG4gICAgICBwb3NpdGlvbjogZml4ZWQ7XG4gICAgICBmbGV4LXNocmluazogMTtcbiAgICAgIHotaW5kZXg6IHZhcigtLWxheWVyLCAwKTtcbiAgICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlKHZhcigtLWR4KSwgdmFyKC0tZHkpKTtcbiAgICB9XG4gICAgQG1lZGlhIChwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyaykge1xuICAgICAgbWFpbiB7XG4gICAgICAgIC0tZ3JpZC1iYWNrZ3JvdW5kLWNvbG9yOiBibGFjaztcbiAgICAgICAgLS1ncmlkLWNvbG9yOiBncmV5O1xuICAgICAgfVxuICAgIH1cbiAgYDtcblxuICByZW5kZXIoKSB7XG4gICAgY29uc29sZS5sb2coXCJyZW5kZXJcIik7XG4gICAgcmV0dXJuIGh0bWxgXG4gICAgICA8bWFpbiBjbGFzcz1cImZ1bGwtc2l6ZVwiPlxuICAgICAgICA8Y2FudmFzIGNsYXNzPVwiZnVsbC1zaXplXCI-PC9jYW52YXM-XG4gICAgICAgIDxkaXYgaWQ9XCJjaGlsZHJlblwiIGNsYXNzPVwiZnVsbC1zaXplXCI-PC9kaXY-XG4gICAgICA8L21haW4-XG4gICAgYDtcbiAgfVxuXG4gIGhhbmRsZURvd24oZXZlbnQ6IFBvaW50ZXJFdmVudCwgdHlwZTogRHJhZ1R5cGUpIHtcbiAgICBpZiAodGhpcy5kcmFnVHlwZSA9PT0gXCJub25lXCIpIHtcbiAgICAgIGV2ZW50LnByZXZlbnREZWZhdWx0KCk7XG4gICAgICB0aGlzLmRyYWdUeXBlID0gdHlwZTtcbiAgICAgIChldmVudC50YXJnZXQgYXMgRWxlbWVudCkuc2V0UG9pbnRlckNhcHR1cmUoZXZlbnQucG9pbnRlcklkKTtcbiAgICAgIHRoaXMucG9pbnRlck1hcC5zZXQoZXZlbnQucG9pbnRlcklkLCB7XG4gICAgICAgIGlkOiBldmVudC5wb2ludGVySWQsXG4gICAgICAgIHN0YXJ0UG9zOiB7IHg6IGV2ZW50LmNsaWVudFgsIHk6IGV2ZW50LmNsaWVudFkgfSxcbiAgICAgICAgY3VycmVudFBvczogeyB4OiBldmVudC5jbGllbnRYLCB5OiBldmVudC5jbGllbnRZIH0sXG4gICAgICB9KTtcbiAgICB9XG4gIH1cblxuICBoYW5kbGVNb3ZlKFxuICAgIGV2ZW50OiBQb2ludGVyRXZlbnQsXG4gICAgdHlwZTogRHJhZ1R5cGUsXG4gICAgb25Nb3ZlOiAoZGVsdGE6IE9mZnNldCkgPT4gdm9pZFxuICApIHtcbiAgICBpZiAodGhpcy5kcmFnVHlwZSA9PT0gdHlwZSkge1xuICAgICAgZXZlbnQucHJldmVudERlZmF1bHQoKTtcbiAgICAgIGNvbnN0IHNhdmVkID0gdGhpcy5wb2ludGVyTWFwLmdldChldmVudC5wb2ludGVySWQpITtcbiAgICAgIGNvbnN0IGN1cnJlbnQgPSB7IC4uLnNhdmVkLmN1cnJlbnRQb3MgfTtcbiAgICAgIHNhdmVkLmN1cnJlbnRQb3MgPSB7IHg6IGV2ZW50LmNsaWVudFgsIHk6IGV2ZW50LmNsaWVudFkgfTtcbiAgICAgIGNvbnN0IGRlbHRhID0ge1xuICAgICAgICB4OiBzYXZlZC5jdXJyZW50UG9zLnggLSBjdXJyZW50LngsXG4gICAgICAgIHk6IHNhdmVkLmN1cnJlbnRQb3MueSAtIGN1cnJlbnQueSxcbiAgICAgIH07XG4gICAgICBvbk1vdmUoZGVsdGEpO1xuICAgIH1cbiAgfVxuXG4gIGhhbmRsZVVwKGV2ZW50OiBQb2ludGVyRXZlbnQpIHtcbiAgICB0aGlzLmRyYWdUeXBlID0gXCJub25lXCI7XG4gICAgKGV2ZW50LnRhcmdldCBhcyBFbGVtZW50KS5yZWxlYXNlUG9pbnRlckNhcHR1cmUoZXZlbnQucG9pbnRlcklkKTtcbiAgfVxuXG4gIG1vdmVDYW52YXMoZGVsdGE6IE9mZnNldCkge1xuICAgIHRoaXMub2Zmc2V0LnggKz0gZGVsdGEueDtcbiAgICB0aGlzLm9mZnNldC55ICs9IGRlbHRhLnk7XG4gICAgdGhpcy5yb290LnN0eWxlLnNldFByb3BlcnR5KFwiLS1vZmZzZXQteFwiLCBgJHt0aGlzLm9mZnNldC54fXB4YCk7XG4gICAgdGhpcy5yb290LnN0eWxlLnNldFByb3BlcnR5KFwiLS1vZmZzZXQteVwiLCBgJHt0aGlzLm9mZnNldC55fXB4YCk7XG4gIH1cblxuICBtb3ZlRWxlbWVudChjaGlsZDogU3VwcG9ydGVkTm9kZSwgZGVsdGE6IE9mZnNldCkge1xuICAgIGNvbnN0IGdldE51bWJlciA9IChrZXk6IFwiLS1keFwiIHwgXCItLWR5XCIsIGZhbGxiYWNrOiBudW1iZXIpID0-IHtcbiAgICAgIGNvbnN0IHNhdmVkID0gY2hpbGQuc3R5bGUuZ2V0UHJvcGVydHlWYWx1ZShrZXkpO1xuICAgICAgaWYgKHNhdmVkLmxlbmd0aCA-IDApIHtcbiAgICAgICAgcmV0dXJuIHBhcnNlRmxvYXQoc2F2ZWQucmVwbGFjZShcInB4XCIsIFwiXCIpKTtcbiAgICAgIH1cbiAgICAgIHJldHVybiBmYWxsYmFjaztcbiAgICB9O1xuICAgIGNvbnN0IGR4ID0gZ2V0TnVtYmVyKFwiLS1keFwiLCAwKSArIGRlbHRhLng7XG4gICAgY29uc3QgZHkgPSBnZXROdW1iZXIoXCItLWR5XCIsIDApICsgZGVsdGEueTtcbiAgICBjaGlsZC5zdHlsZS50cmFuc2Zvcm0gPSBgdHJhbnNsYXRlKCR7ZHh9cHgsICR7ZHl9cHgpYDtcbiAgICBjaGlsZC5zdHlsZS5zZXRQcm9wZXJ0eShcIi0tZHhcIiwgYCR7ZHh9cHhgKTtcbiAgICBjaGlsZC5zdHlsZS5zZXRQcm9wZXJ0eShcIi0tZHlcIiwgYCR7ZHl9cHhgKTtcbiAgfVxuXG4gIGFzeW5jIGZpcnN0VXBkYXRlZCgpIHtcbiAgICBjb25zdCBpdGVtcyA9IEFycmF5LmZyb20odGhpcy5jaGlsZE5vZGVzKTtcbiAgICBsZXQgaSA9IDA7XG4gICAgZm9yIChjb25zdCBub2RlIG9mIGl0ZW1zKSB7XG4gICAgICBpZiAobm9kZSBpbnN0YW5jZW9mIFNWR0VsZW1lbnQgfHwgbm9kZSBpbnN0YW5jZW9mIEhUTUxFbGVtZW50KSB7XG4gICAgICAgIGNvbnN0IGNoaWxkID0gbm9kZSBhcyBTdXBwb3J0ZWROb2RlO1xuICAgICAgICBjaGlsZC5jbGFzc0xpc3QuYWRkKFwiY2hpbGRcIik7XG4gICAgICAgIGNoaWxkLnN0eWxlLnNldFByb3BlcnR5KFwiLS1sYXllclwiLCBgJHtpfWApO1xuICAgICAgICB0aGlzLmNvbnRhaW5lci5hcHBlbmQoY2hpbGQpO1xuICAgICAgICBjaGlsZC5hZGRFdmVudExpc3RlbmVyKFwicG9pbnRlcmRvd25cIiwgKGU6IGFueSkgPT4ge1xuICAgICAgICAgIHRoaXMuaGFuZGxlRG93bihlLCBcImVsZW1lbnRcIik7XG4gICAgICAgIH0pO1xuICAgICAgICBjaGlsZC5hZGRFdmVudExpc3RlbmVyKFwicG9pbnRlcm1vdmVcIiwgKGU6IGFueSkgPT4ge1xuICAgICAgICAgIHRoaXMuaGFuZGxlTW92ZShlLCBcImVsZW1lbnRcIiwgKGRlbHRhKSA9PiB7XG4gICAgICAgICAgICB0aGlzLm1vdmVFbGVtZW50KGNoaWxkLCBkZWx0YSk7XG4gICAgICAgICAgfSk7XG4gICAgICAgIH0pO1xuICAgICAgICBpKys7XG4gICAgICB9XG4gICAgfVxuICAgIHRoaXMucmVxdWVzdFVwZGF0ZSgpO1xuICAgIHRoaXMucm9vdC5hZGRFdmVudExpc3RlbmVyKFwicG9pbnRlcmRvd25cIiwgKGU6IGFueSkgPT4ge1xuICAgICAgdGhpcy5oYW5kbGVEb3duKGUsIFwiY2FudmFzXCIpO1xuICAgIH0pO1xuICAgIHRoaXMucm9vdC5hZGRFdmVudExpc3RlbmVyKFwicG9pbnRlcm1vdmVcIiwgKGU6IGFueSkgPT4ge1xuICAgICAgdGhpcy5oYW5kbGVNb3ZlKGUsIFwiY2FudmFzXCIsIChkZWx0YSkgPT4ge1xuICAgICAgICB0aGlzLm1vdmVDYW52YXMoZGVsdGEpO1xuICAgICAgICBmb3IgKGNvbnN0IG5vZGUgb2YgQXJyYXkuZnJvbSh0aGlzLmNvbnRhaW5lci5jaGlsZHJlbikpIHtcbiAgICAgICAgICBpZiAobm9kZSBpbnN0YW5jZW9mIFNWR0VsZW1lbnQgfHwgbm9kZSBpbnN0YW5jZW9mIEhUTUxFbGVtZW50KSB7XG4gICAgICAgICAgICB0aGlzLm1vdmVFbGVtZW50KG5vZGUsIGRlbHRhKTtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH0pO1xuICAgIH0pO1xuICAgIHRoaXMucm9vdC5hZGRFdmVudExpc3RlbmVyKFwicG9pbnRlcnVwXCIsIChlOiBhbnkpID0-IHtcbiAgICAgIHRoaXMuaGFuZGxlVXAoZSk7XG4gICAgfSk7XG4gIH1cbn1cblxuaW50ZXJmYWNlIE9mZnNldCB7XG4gIHg6IG51bWJlcjtcbiAgeTogbnVtYmVyO1xufVxuXG5pbnRlcmZhY2UgUG9pbnRlckRhdGEge1xuICBpZDogbnVtYmVyO1xuICBzdGFydFBvczogT2Zmc2V0O1xuICBjdXJyZW50UG9zOiBPZmZzZXQ7XG59XG4ifSx7Im5hbWUiOiJpbmRleC5odG1sIiwiY29udGVudCI6IjwhRE9DVFlQRSBodG1sPlxuPGh0bWwgbGFuZz1cImVuXCI-XG4gIDxoZWFkPlxuICAgIDxtZXRhIGNoYXJzZXQ9XCJVVEYtOFwiIC8-XG4gICAgPGxpbmsgcmVsPVwiaWNvblwiIHR5cGU9XCJpbWFnZS9zdmcreG1sXCIgaHJlZj1cIi9zcmMvZmF2aWNvbi5zdmdcIiAvPlxuICAgIDxtZXRhIG5hbWU9XCJ2aWV3cG9ydFwiIGNvbnRlbnQ9XCJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MS4wXCIgLz5cbiAgICA8dGl0bGU-TGl0IENTUyBDYW52YXM8L3RpdGxlPlxuICAgIDxzdHlsZT5cbiAgICAgIGJvZHkge1xuICAgICAgICBtYXJnaW46IDA7XG4gICAgICAgIHBhZGRpbmc6IDA7XG4gICAgICAgIHdpZHRoOiAxMDAlO1xuICAgICAgICBoZWlnaHQ6IDEwMHZoO1xuICAgICAgfVxuICAgIDwvc3R5bGU-XG4gICAgPHNjcmlwdCB0eXBlPVwibW9kdWxlXCIgc3JjPVwiLi9saXQtY3NzLWNhbnZhcy5qc1wiPjwvc2NyaXB0PlxuICA8L2hlYWQ-XG4gIDxib2R5PlxuICAgIDxjc3MtY2FudmFzPlxuICAgICAgPGltZ1xuICAgICAgICBzcmM9XCJodHRwczovL2xpdC5kZXYvaW1hZ2VzL2xvZ28uc3ZnXCJcbiAgICAgICAgYWx0PVwiTGl0IExvZ29cIlxuICAgICAgICB3aWR0aD1cIjUwMFwiXG4gICAgICAgIGhlaWdodD1cIjMzM1wiXG4gICAgICAgIHN0eWxlPVwiLS1keDogNTkuNDkwOXB4OyAtLWR5OiAzMi44NDI5cHhcIlxuICAgICAgLz5cbiAgICAgIDxzdmcgd2lkdGg9XCI0MDBcIiBoZWlnaHQ9XCIxMTBcIiBzdHlsZT1cIi0tZHg6IDIzMC4wNTdweDsgLS1keTogMzMuNjI1N3B4XCI-XG4gICAgICAgIDxyZWN0XG4gICAgICAgICAgd2lkdGg9XCI0MDBcIlxuICAgICAgICAgIGhlaWdodD1cIjEwMFwiXG4gICAgICAgICAgc3R5bGU9XCJmaWxsOiByZ2IoMCwgMCwgMjU1KTsgc3Ryb2tlLXdpZHRoOiAzOyBzdHJva2U6IHJnYigwLCAwLCAwKVwiXG4gICAgICAgIC8-XG4gICAgICA8L3N2Zz5cbiAgICAgIDxzdmcgaGVpZ2h0PVwiMTAwXCIgd2lkdGg9XCIxMDBcIj5cbiAgICAgICAgPGNpcmNsZVxuICAgICAgICAgIGN4PVwiNTBcIlxuICAgICAgICAgIGN5PVwiNTBcIlxuICAgICAgICAgIHI9XCI0MFwiXG4gICAgICAgICAgc3Ryb2tlPVwiYmxhY2tcIlxuICAgICAgICAgIHN0cm9rZS13aWR0aD1cIjNcIlxuICAgICAgICAgIGZpbGw9XCJyZWRcIlxuICAgICAgICAvPlxuICAgICAgPC9zdmc-XG4gICAgPC9jc3MtY2FudmFzPlxuICA8L2JvZHk-XG48L2h0bWw-XG4ifV0&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The source for this example can be found &lt;a href=&quot;https://github.com/rodydavis/lit-draggable-dom&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Lit and Figma</title><link>https://rodydavis.com/lit/figma-interop/</link><guid isPermaLink="true">https://rodydavis.com/lit/figma-interop/</guid><description>This article details how to create a Figma plugin using Lit, a web component framework, including setup instructions and a link to the complete source code.</description><pubDate>Mon, 20 Jan 2025 02:00:23 GMT</pubDate><content:encoded>&lt;h1&gt;Lit and Figma&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to set up a &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; web component and use it to create a figma plugin.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; You can find the final source &lt;a href=&quot;https://github.com/rodydavis/figma_lit_example&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Figma Desktop&lt;/li&gt;
&lt;li&gt;Node&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by creating a empty directory and naming it with &lt;code&gt;snake_case&lt;/code&gt; whatever we want.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir figma_lit_example
cd figma_lit_example
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Web Setup&lt;/h3&gt;
&lt;p&gt;Now we are in the &lt;code&gt;figma_lit_example&lt;/code&gt; directory and can setup Figma and Lit. Let&apos;s start with node.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will setup the basics for a node project and install the packages we need. Now lets add some config files. Now open the &lt;code&gt;package.json&lt;/code&gt; and replace it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &amp;quot;name&amp;quot;: &amp;quot;figma_lit_example&amp;quot;,
  &amp;quot;version&amp;quot;: &amp;quot;1.0.0&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;Lit Figma Plugin&amp;quot;,
  &amp;quot;dependencies&amp;quot;: {
    &amp;quot;lit&amp;quot;: &amp;quot;^2.0.0-rc.1&amp;quot;
  },
  &amp;quot;devDependencies&amp;quot;: {
    &amp;quot;@figma/plugin-typings&amp;quot;: &amp;quot;^1.23.0&amp;quot;,
    &amp;quot;html-webpack-inline-source-plugin&amp;quot;: &amp;quot;^1.0.0-beta.2&amp;quot;,
    &amp;quot;html-webpack-plugin&amp;quot;: &amp;quot;^4.3.0&amp;quot;,
    &amp;quot;css-loader&amp;quot;: &amp;quot;^5.2.4&amp;quot;,
    &amp;quot;ts-loader&amp;quot;: &amp;quot;^8.0.0&amp;quot;,
    &amp;quot;typescript&amp;quot;: &amp;quot;^4.2.4&amp;quot;,
    &amp;quot;url-loader&amp;quot;: &amp;quot;^4.1.1&amp;quot;,
    &amp;quot;webpack&amp;quot;: &amp;quot;^4.44.1&amp;quot;,
    &amp;quot;webpack-cli&amp;quot;: &amp;quot;^4.6.0&amp;quot;
  },
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;dev&amp;quot;: &amp;quot;npx webpack --mode=development --watch&amp;quot;,
    &amp;quot;copy&amp;quot;: &amp;quot;mkdir -p lit-plugin &amp;amp;&amp;amp; cp ./manifest.json ./lit-plugin/manifest.json &amp;amp;&amp;amp; cp ./dist/ui.html ./lit-plugin/ui.html &amp;amp;&amp;amp; cp ./dist/code.js ./lit-plugin/code.js&amp;quot;,
    &amp;quot;build&amp;quot;: &amp;quot;npx webpack --mode=production &amp;amp;&amp;amp; npm run copy&amp;quot;,
    &amp;quot;zip&amp;quot;: &amp;quot;npm run build &amp;amp;&amp;amp; zip -r lit-plugin.zip lit-plugin&amp;quot;
  },
  &amp;quot;browserslist&amp;quot;: [
    &amp;quot;last 1 Chrome versions&amp;quot;
  ],
  &amp;quot;keywords&amp;quot;: [],
  &amp;quot;author&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;license&amp;quot;: &amp;quot;ISC&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will add everything we need and add the scripts we need for development and production. Then run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will install everything we need to get started. Now we need to setup some config files.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;touch tsconfig.json
touch webpack.config.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create 2 files. Now open up &lt;code&gt;tsconfig.json&lt;/code&gt; and paste the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &amp;quot;compilerOptions&amp;quot;: {
    &amp;quot;target&amp;quot;: &amp;quot;es2017&amp;quot;,
    &amp;quot;module&amp;quot;: &amp;quot;esNext&amp;quot;,
    &amp;quot;moduleResolution&amp;quot;: &amp;quot;node&amp;quot;,
    &amp;quot;lib&amp;quot;: [&amp;quot;es2017&amp;quot;, &amp;quot;dom&amp;quot;, &amp;quot;dom.iterable&amp;quot;],
    &amp;quot;typeRoots&amp;quot;: [&amp;quot;./node_modules/@types&amp;quot;, &amp;quot;./node_modules/@figma&amp;quot;],
    &amp;quot;declaration&amp;quot;: true,
    &amp;quot;sourceMap&amp;quot;: true,
    &amp;quot;inlineSources&amp;quot;: true,
    &amp;quot;noUnusedLocals&amp;quot;: true,
    &amp;quot;noImplicitReturns&amp;quot;: true,
    &amp;quot;noFallthroughCasesInSwitch&amp;quot;: true,
    &amp;quot;experimentalDecorators&amp;quot;: true,
    &amp;quot;skipLibCheck&amp;quot;: true,
    &amp;quot;strict&amp;quot;: true,
    &amp;quot;noImplicitAny&amp;quot;: false,
    &amp;quot;outDir&amp;quot;: &amp;quot;./lib&amp;quot;,
    &amp;quot;baseUrl&amp;quot;: &amp;quot;./packages&amp;quot;,
    &amp;quot;importHelpers&amp;quot;: true,
    &amp;quot;plugins&amp;quot;: [
      {
        &amp;quot;name&amp;quot;: &amp;quot;ts-lit-plugin&amp;quot;,
        &amp;quot;rules&amp;quot;: {
          &amp;quot;no-unknown-tag-name&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-unclosed-tag&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-unknown-property&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-unintended-mixed-binding&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-invalid-boolean-binding&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-expressionless-property-binding&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-noncallable-event-binding&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-boolean-in-attribute-binding&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-complex-attribute-binding&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-nullable-attribute-binding&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-incompatible-type-binding&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-invalid-directive-binding&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-incompatible-property-type&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-unknown-property-converter&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-invalid-attribute-name&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-invalid-tag-name&amp;quot;: &amp;quot;error&amp;quot;,
          &amp;quot;no-unknown-attribute&amp;quot;: &amp;quot;off&amp;quot;,
          &amp;quot;no-unknown-event&amp;quot;: &amp;quot;off&amp;quot;,
          &amp;quot;no-unknown-slot&amp;quot;: &amp;quot;off&amp;quot;,
          &amp;quot;no-invalid-css&amp;quot;: &amp;quot;off&amp;quot;
        }
      }
    ]
  },
  &amp;quot;include&amp;quot;: [&amp;quot;src/**/*.ts&amp;quot;],
  &amp;quot;references&amp;quot;: []
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a basic typescript config. Now open up &lt;code&gt;webpack.config.ts&lt;/code&gt; and paste the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const HtmlWebpackInlineSourcePlugin = require(&amp;quot;html-webpack-inline-source-plugin&amp;quot;);
const HtmlWebpackPlugin = require(&amp;quot;html-webpack-plugin&amp;quot;);
const path = require(&amp;quot;path&amp;quot;);

module.exports = (env, argv) =&amp;gt; ({
  mode: argv.mode === &amp;quot;production&amp;quot; ? &amp;quot;production&amp;quot; : &amp;quot;development&amp;quot;,
  devtool: argv.mode === &amp;quot;production&amp;quot; ? false : &amp;quot;inline-source-map&amp;quot;,
  entry: {
    ui: &amp;quot;./src/ui.ts&amp;quot;,
    code: &amp;quot;./src/code.ts&amp;quot;,
    app: &amp;quot;./src/my-app.ts&amp;quot;,
  },
  module: {
    rules: [
      { test: /\.tsx?$/, use: &amp;quot;ts-loader&amp;quot;, exclude: /node_modules/ },
      { test: /\.css$/, use: [&amp;quot;style-loader&amp;quot;, { loader: &amp;quot;css-loader&amp;quot; }] },
      { test: /\.(png|jpg|gif|webp|svg)$/, loader: &amp;quot;url-loader&amp;quot; },
    ],
  },
  resolve: { extensions: [&amp;quot;.ts&amp;quot;, &amp;quot;.js&amp;quot;] },
  output: {
    filename: &amp;quot;[name].js&amp;quot;,
    path: path.resolve(__dirname, &amp;quot;dist&amp;quot;),
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, &amp;quot;ui.html&amp;quot;),
      filename: &amp;quot;ui.html&amp;quot;,
      inject: true,
      inlineSource: &amp;quot;.(js|css)$&amp;quot;,
      chunks: [&amp;quot;ui&amp;quot;],
    }),
    new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),
  ],
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to create the ui for the plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;touch ui.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open up &lt;code&gt;/src/ui.html&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;my-app&amp;gt;&amp;lt;/my-app&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need a manifest file for the figma plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;touch manifest.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;manifest.json&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &amp;quot;name&amp;quot;: &amp;quot;figma_lit_example&amp;quot;,
  &amp;quot;id&amp;quot;: &amp;quot;973668777853442323&amp;quot;,
  &amp;quot;api&amp;quot;: &amp;quot;1.0.0&amp;quot;,
  &amp;quot;main&amp;quot;: &amp;quot;code.js&amp;quot;,
  &amp;quot;ui&amp;quot;: &amp;quot;ui.html&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to create our web component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir src
cd src
touch my-app.ts
touch code.ts
touch ui.ts
cd ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;/src/ui.ts&lt;/code&gt; and paste the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import &amp;quot;./my-app&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;/src/my-app.ts&lt;/code&gt; and paste the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, query } from &amp;quot;lit/decorators.js&amp;quot;;

@customElement(&amp;quot;my-app&amp;quot;)
export class MyApp extends LitElement {
  @property() amount = &amp;quot;5&amp;quot;;
  @query(&amp;quot;#count&amp;quot;) countInput!: HTMLInputElement;

  render() {
    return html`
      &amp;lt;div&amp;gt;
        &amp;lt;h2&amp;gt;Rectangle Creator&amp;lt;/h2&amp;gt;
        &amp;lt;p&amp;gt;Count: &amp;lt;input id=&amp;quot;count&amp;quot; value=&amp;quot;${this.amount}&amp;quot; /&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;button id=&amp;quot;create&amp;quot; @click=${this.create}&amp;gt;Create&amp;lt;/button&amp;gt;
        &amp;lt;button id=&amp;quot;cancel&amp;quot; @click=${this.cancel}&amp;gt;Cancel&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    `;
  }

  create() {
    const count = parseInt(this.countInput.value, 10);
    this.sendMessage(&amp;quot;create-rectangles&amp;quot;, { count });
  }

  cancel() {
    this.sendMessage(&amp;quot;cancel&amp;quot;);
  }

  private sendMessage(type: string, content: Object = {}) {
    const message = { pluginMessage: { type: type, ...content } };
    parent.postMessage(message, &amp;quot;*&amp;quot;);
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;code.ts&lt;/code&gt; and paste the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const options: ShowUIOptions = {
  width: 250,
  height: 200,
};

figma.showUI(__html__, options);

figma.ui.onmessage = msg =&amp;gt; {
  switch (msg.type) {
    case &apos;create-rectangles&apos;:
      const nodes: SceneNode[] = [];
      for (let i = 0; i &amp;lt; msg.count; i++) {
        const rect = figma.createRectangle();
        rect.x = i * 150;
        rect.fills = [{ type: &apos;SOLID&apos;, color: { r: 1, g: 0.5, b: 0 } }];
        figma.currentPage.appendChild(rect);
        nodes.push(rect);
      }
      figma.currentPage.selection = nodes;
      figma.viewport.scrollAndZoomIntoView(nodes);
      break;
    default:
      break;
  }

  figma.closePlugin();
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Building the Plugin&lt;/h2&gt;
&lt;p&gt;Now that we have all the code in place we can build the plugin and test it in Figma.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 1&lt;/h4&gt;
&lt;p&gt;Download and open the desktop version of Figma.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.figma.com/downloads/&quot;&gt;https://www.figma.com/downloads/&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Step 2&lt;/h4&gt;
&lt;p&gt;Open the menu and navigate to “Plugins &amp;gt; Manage plugins”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/f_1_kfp7k3aclm.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Step 3&lt;/h4&gt;
&lt;p&gt;Click on the plus icon to add a local plugin.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/f_2_m4hgctvnry.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the box to link to an existing plugin to navigate to the &lt;code&gt;lit-plugin&lt;/code&gt; folder that was created after the build process in your source code and select &lt;code&gt;manifest.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/f_3_ufrd6v1644.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Step 4&lt;/h4&gt;
&lt;p&gt;To run the plugin navigate to “Plugins &amp;gt; Development &amp;gt; figma_lit_example” to launch your plugin.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/f_4_uoz37dtck3.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Step 5&lt;/h4&gt;
&lt;p&gt;Now your plugin should launch and you can create 5 rectangles on the canvas.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/f_5_496mr4f5wp.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;If everything worked you will have 5 new rectangles on the canvas focused by figma.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/f_6_kp2j5tc6xw.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;WASM Support&lt;/h2&gt;
&lt;p&gt;If there is a heavy computation that could benefit from running in &lt;a href=&quot;https://webassembly.org/&quot;&gt;WebAssembly&lt;/a&gt; the following will ensure that it is hardware accelerated when possible.&lt;/p&gt;
&lt;p&gt;Let&apos;s add &lt;a href=&quot;https://www.assemblyscript.org/&quot;&gt;AssemblyScript&lt;/a&gt; and some dependencies that will be used for loading the WASM into the figma ui.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm i @assemblyscript/loader
npm i --D assemblyscript js-inline-wasm
npx asinit .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Confirm yes to the prompt to have it generate the project files and add the following to the scripts in &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&amp;quot;asbuild:untouched&amp;quot;: &amp;quot;asc assembly/index.ts --target debug&amp;quot;,
&amp;quot;asbuild:optimized&amp;quot;: &amp;quot;asc assembly/index.ts --target release&amp;quot;,
&amp;quot;asbuild&amp;quot;: &amp;quot;npm run asbuild:untouched &amp;amp;&amp;amp; npm run asbuild:optimized&amp;quot;,
&amp;quot;inlinewasm&amp;quot;: &amp;quot;inlinewasm build/optimized.wasm --output src/wasm.ts&amp;quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code that will be used for the WASM is in &lt;code&gt;/assembly/index.ts&lt;/code&gt; and it should show the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// The entry file of your WebAssembly module.

export function add(a: i32, b: i32): i32 {
  return a + b;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let&apos;s build the wasm module:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm run asbuild
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the wasm build to be ignored for git add the following to .gitignore:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will generate the wasm and wat files in the build directory, but for figma to load them into the ui it needs to be inlined so run the following command to generate the js from the wasm file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm run inlinewasm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should generate &lt;code&gt;src/wasm.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const encoded = &apos;AGFzbQEAAAABBwFgAn9/AX8DAgEABQMBAAAHEAIDYWRkAAAGbWVtb3J5AgAKCQEHACAAIAFqCwAmEHNvdXJjZU1hcHBpbmdVUkwULi9vcHRpbWl6ZWQud2FzbS5tYXA=&apos;;
export default new Promise(resolve =&amp;gt; {
    const decoded = atob(encoded);
    const len = decoded.length;
    const bytes = new Uint8Array(len);
    for (var i = 0; i &amp;lt; len; i++) {
        bytes[i] = decoded.charCodeAt(i);
    }
    resolve(new Response(bytes, { status: 200, headers: { &amp;quot;Content-Type&amp;quot;: &amp;quot;application/wasm&amp;quot; } }));
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now open up the &lt;code&gt;/src/my-app.ts&lt;/code&gt; and update with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, property, query } from &amp;quot;lit/decorators.js&amp;quot;;

@customElement(&amp;quot;my-app&amp;quot;)
export class MyApp extends LitElement {
  @property() amount = &amp;quot;5&amp;quot;; // &amp;lt;-- Pass in a value for the number of rectangles to create
  @query(&amp;quot;#count&amp;quot;) countInput!: HTMLInputElement;

  render() {
    return html`
      &amp;lt;div&amp;gt;
        &amp;lt;h2&amp;gt;Rectangle Creator&amp;lt;/h2&amp;gt;
        &amp;lt;!-- Pass in the amount to the input value --&amp;gt;
        &amp;lt;p&amp;gt;Count: &amp;lt;input id=&amp;quot;count&amp;quot; value=&amp;quot;${this.amount}&amp;quot; /&amp;gt;&amp;lt;/p&amp;gt;
        ...
      &amp;lt;/div&amp;gt;
    `;
  }
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will let us pass in the amount of boxes to create externally.&lt;/p&gt;
&lt;p&gt;Now open &lt;code&gt;/src/ui.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import &amp;quot;./my-app&amp;quot;;

import wasm from &amp;quot;./wasm&amp;quot;; // &amp;lt;-- Our WASM file to load

WebAssembly.instantiateStreaming(wasm as Promise&amp;lt;Response&amp;gt;).then((obj) =&amp;gt; {
  // @ts-ignore
  const value: number = obj.instance.exports.add(2, 4);
  console.log(&amp;quot;return from wasm&amp;quot;, value);
  const elem = document.querySelector(&apos;my-app&apos;)! as HTMLElement;
  elem.setAttribute(&apos;amount&apos;, `${value}`);
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when we build the plugin and run it in figma the amount of boxes will be the result of calling into wasm!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to learn more about building a plugin in Figma you can read more &lt;a href=&quot;https://www.figma.com/plugin-docs/intro/&quot;&gt;here&lt;/a&gt; and for Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>2D or 3D Force Graph with Lit</title><link>https://rodydavis.com/lit/force-graph/</link><guid isPermaLink="true">https://rodydavis.com/lit/force-graph/</guid><description>Create interactive 2D/3D force graphs using Lit, a web component framework, with a detailed guide including setup, configuration, and a working template.</description><pubDate>Sun, 19 Jan 2025 06:03:11 GMT</pubDate><content:encoded>&lt;h1&gt;2D or 3D Force Graph with Lit&lt;/h1&gt;
&lt;p&gt;In this article we will cover how to create a 2D/3D force graph using &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/lit-force-graph&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/lit-force-graph/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Node &amp;gt;= 16&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by navigating in terminal to the location of the project and run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init @vitejs/app --template lit-ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enter a project name &lt;code&gt;lit-force-graph&lt;/code&gt; and now open the project in vscode and install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd lit-force-graph force-graph
npm i lit 3d-force-graph
npm i -D @types/node
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;vite.config.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from &amp;quot;vite&amp;quot;;
import { resolve } from &amp;quot;path&amp;quot;;

export default defineConfig({
  base: &amp;quot;/lit-force-graph/&amp;quot;,
  build: {
    lib: {
      entry: &amp;quot;src/lit-force-graph.ts&amp;quot;,
      formats: [&amp;quot;es&amp;quot;],
    },
    rollupOptions: {
      input: {
        main: resolve(__dirname, &amp;quot;index.html&amp;quot;),
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Template&lt;/h2&gt;
&lt;p&gt;Open up the &lt;code&gt;index.html&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/src/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;Lit Force Graph&amp;lt;/title&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/lit-force-graph.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;/style.css&amp;quot; /&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;lit-force-graph&amp;gt;
      &amp;lt;script type=&amp;quot;application/json&amp;quot;&amp;gt;
        {
          &amp;quot;name&amp;quot;: &amp;quot;Lit Force Graph&amp;quot;,
          &amp;quot;description&amp;quot;: &amp;quot;A force graph built with Lit&amp;quot;,
          &amp;quot;nodes&amp;quot;: [
            {
              &amp;quot;id&amp;quot;: &amp;quot;1&amp;quot;,
              &amp;quot;name&amp;quot;: &amp;quot;Node 1&amp;quot;
            },
            {
              &amp;quot;id&amp;quot;: &amp;quot;2&amp;quot;,
              &amp;quot;name&amp;quot;: &amp;quot;Node 2&amp;quot;
            },
            {
              &amp;quot;id&amp;quot;: &amp;quot;3&amp;quot;,
              &amp;quot;name&amp;quot;: &amp;quot;Node 3&amp;quot;
            },
            {
              &amp;quot;id&amp;quot;: &amp;quot;4&amp;quot;,
              &amp;quot;name&amp;quot;: &amp;quot;Node 4&amp;quot;
            }
          ],
          &amp;quot;links&amp;quot;: [
            {
              &amp;quot;source&amp;quot;: &amp;quot;1&amp;quot;,
              &amp;quot;target&amp;quot;: &amp;quot;2&amp;quot;
            },
            {
              &amp;quot;source&amp;quot;: &amp;quot;1&amp;quot;,
              &amp;quot;target&amp;quot;: &amp;quot;3&amp;quot;
            },
            {
              &amp;quot;source&amp;quot;: &amp;quot;2&amp;quot;,
              &amp;quot;target&amp;quot;: &amp;quot;3&amp;quot;
            },
            {
              &amp;quot;source&amp;quot;: &amp;quot;2&amp;quot;,
              &amp;quot;target&amp;quot;: &amp;quot;4&amp;quot;
            },
            {
              &amp;quot;source&amp;quot;: &amp;quot;3&amp;quot;,
              &amp;quot;target&amp;quot;: &amp;quot;4&amp;quot;
            },
            {
              &amp;quot;source&amp;quot;: &amp;quot;4&amp;quot;,
              &amp;quot;target&amp;quot;: &amp;quot;1&amp;quot;
            }
          ]
        }
      &amp;lt;/script&amp;gt;
    &amp;lt;/lit-force-graph&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are passing the graph data as JSON here, but we could also set a src attribute pointed to a remote or local file. It is still possible to set the graph data directly on a component.&lt;/p&gt;
&lt;h2&gt;Styles&lt;/h2&gt;
&lt;p&gt;Create and open the &lt;code&gt;public/style.css&lt;/code&gt; file and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;body {
  margin: 0;
  padding: 0;
  overflow: hidden;
  font-size: 12px;
  font-family: sans-serif;
  position: relative;
  width: 100%;
  height: 100%;
}

lit-force-graph {
  width: 100%;
  height: 100vh;
}

:root {
  --graph-background-color: #eee;
  --graph-foreground-color: #000;
  --graph-line-color: rgb(90, 90, 90);
  --graph-node-color: rgb(218, 14, 14);
}

@media (prefers-color-scheme: dark) {
  :root {
    --graph-background-color: #000;
    --graph-foreground-color: #fafafa;
    --graph-line-color: rgb(214, 214, 214);
    --graph-node-color: rgb(228, 8, 8);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Web Component&lt;/h2&gt;
&lt;p&gt;Before we update our component we need to rename &lt;code&gt;my-element.ts&lt;/code&gt; to &lt;code&gt;lit-force-graph.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open up &lt;code&gt;lit-force-graph.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, css, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, property, query, state } from &amp;quot;lit/decorators.js&amp;quot;;

export const tagName = &amp;quot;lit-force-graph&amp;quot;;

@customElement(tagName)
export class LitForceGraph extends LitElement {
  static styles = css`
    :host {
      background-color: var(--graph-background-color, #000011);
      color: var(--graph-foreground-color, #ffffff);
      width: var(--graph-width, 100%);
      height: var(--graph-height, 100vh);
    }

    #graph {
      width: 100%;
      height: 100%;
      width: var(--graph-width, 100%);
      height: var(--graph-height, 100vh);
    }

    #controls {
      position: absolute;
      top: 20px;
      right: 20px;
      z-index: 100 !important;
      display: flex;
      flex-direction: column;
      align-items: flex-end;
    }
    #controls div {
      padding: 5px;
    }

    #info {
      position: absolute;
      top: 10px;
      left: 10px;
      z-index: 100 !important;
      display: flex;
      flex-direction: column;
      align-items: flex-start;
    }

    #tooltips {
      position: absolute;
      bottom: 10px;
      left: 10px;
      right: 10px;
      display: flex;
      flex-direction: row;
      align-items: center;
      text-align: center;
      justify-content: center;
    }

    .node-tooltip {
      background-color: var(--graph-foreground-color, #ffffff);
      color: var(--graph-background-color, #000011);
      border-radius: 5px;
      font-size: 12px;
      padding: 5px;
      opacity: 0.67;
    }

    #graph-description {
      opacity: 0.67;
    }

    .scene-tooltip {
      color: var(--graph-foreground-color, #ffffff);
      background-color: transparent;
      display: none;
    }
  `;

  @query(&amp;quot;#graph&amp;quot;) graph!: HTMLElement;
  @property() src = &amp;quot;&amp;quot;;
  @property() mode = &amp;quot;2D&amp;quot;;

  render() {
    return html` &amp;lt;main
      accept=&amp;quot;application/json&amp;quot;
      @drop=&amp;quot;${this.onDrop}&amp;quot;
      @dragover=&amp;quot;${(e: Event) =&amp;gt; e.preventDefault()}&amp;quot;
    &amp;gt;
      &amp;lt;div id=&amp;quot;graph&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;div id=&amp;quot;controls&amp;quot;&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;label for=&amp;quot;render-mode&amp;quot;&amp;gt;Render mode&amp;lt;/label&amp;gt;
          &amp;lt;select id=&amp;quot;render-mode&amp;quot; @change=${this.onChangeMode}&amp;gt;
            &amp;lt;!-- TODO: Add render options --&amp;gt;
          &amp;lt;/select&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div id=&amp;quot;info&amp;quot;&amp;gt;
        &amp;lt;!-- TODO: Add labels for graph --&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div id=&amp;quot;tooltips&amp;quot;&amp;gt;
        &amp;lt;!-- TODO: Add tooltip for node --&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/main&amp;gt;`;
  }

  override async firstUpdated() {
    await this.refresh();
    const prefersDark = window.matchMedia(&amp;quot;(prefers-color-scheme: dark)&amp;quot;);
    prefersDark.addEventListener(&amp;quot;change&amp;quot;, () =&amp;gt; {
      this.refresh();
    });
  }

  override attributeChangedCallback(
    name: string,
    _old: string | null,
    value: string | null
  ): void {
    if (name === &amp;quot;src&amp;quot; &amp;amp;&amp;amp; value) {
      this.refresh();
    }
    if (name === &amp;quot;data&amp;quot; &amp;amp;&amp;amp; value) {
      this.setData(JSON.parse(value));
    }
    if (name === &amp;quot;mode&amp;quot; &amp;amp;&amp;amp; value) {
      this.mode = value;
      if (this.data) {
        this.setData({ ...this.data! });
      }
    }
    super.attributeChangedCallback(name, _old, value);
  }

  /**
   * Set the graph data and update the renderer
   *
   * @param data Graph JSON
   */
  setData(data: GraphData) {
    this.data = data;
    // TODO: Render the graph!
  }

  private async refresh() {
    // Get json from script tag
    const children = Array.from(this.children);
    const elem = children.find((child) =&amp;gt; child.tagName === &amp;quot;SCRIPT&amp;quot;);
    if (elem) {
      // Render from script tag contents
      if (elem.textContent) {
        const data = JSON.parse(elem.textContent);
        if (data) this.setData(data);
        // Render from script tag src
      } else if (elem.hasAttribute(&amp;quot;src&amp;quot;)) {
        const url = elem.getAttribute(&amp;quot;src&amp;quot;)!;
        const data = await fetch(url).then((res) =&amp;gt; res.json());
        if (data) this.setData(data);
      }
    } else if (this.src.length &amp;gt; 0) {
      // Render from src attribute
      const data = await fetch(this.src).then((res) =&amp;gt; res.json());
      if (data) this.setData(data);
    }
  }

  private onDrop(e: DragEvent) {
    e.preventDefault();
    const files = e.dataTransfer?.files;
    if (files &amp;amp;&amp;amp; files.length &amp;gt; 0) {
      const file = files[0];
      const reader = new FileReader();
      reader.onload = () =&amp;gt; {
        const json = JSON.parse(reader.result as string);
        this.data = json;
        this.setData(json);
      };
      reader.readAsText(file);
    }
    return false;
  }

  private onChangeMode(e: Event) {
    const mode = (e.target as HTMLSelectElement).value;
    this.mode = mode;
    if (!this.data) return;
    this.setData({ ...this.data! });
  }
}

declare global {
  interface HTMLElementTagNameMap {
    &amp;quot;lit-force-graph&amp;quot;: LitForceGraph;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are creating the base component and wiring it up to listen for a drop event of JSON, accept the src attribute or script tag with json in the text contents.&lt;/p&gt;
&lt;p&gt;The CSS just sets the tooltip at the bottom of the screen, title to the left and the render selection controls to the top right.&lt;/p&gt;
&lt;p&gt;With Lit it makes it easy to support multiple ways to set the data of the component.&lt;/p&gt;
&lt;h3&gt;Inline&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;lit-source-graph&amp;gt;
  &amp;lt;script type=&amp;quot;application/json&amp;quot;&amp;gt;
    {
      &amp;quot;nodes&amp;quot;: [],
      &amp;quot;links&amp;quot;: []
    }
  &amp;lt;/script&amp;gt;
&amp;lt;/lit-source-graph&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Lazy Loading&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;lit-source-graph&amp;gt;&amp;lt;/lit-source-graph&amp;gt;
&amp;lt;script&amp;gt;
  const elem = document.createElement(&amp;quot;lit-source-graph&amp;quot;);
  elem.src = &amp;quot;./graph-data.json&amp;quot;;
  // Or remote url
  elem.src = &amp;quot;https://example.com/graph-data.json&amp;quot;;
  // Or data from an object
  elem.data = { node: [], links: [] };
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Graph Data&lt;/h2&gt;
&lt;p&gt;Create and open the file &lt;code&gt;src/classes/graph.ts&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export class Graph {
  private ids = new Set();
  private graph: GraphData = {
    nodes: [],
    links: [],
  };

  addNode&amp;lt;T = any&amp;gt;(node: GraphNode&amp;lt;T&amp;gt;) {
    if (this.ids.has(node.id)) {
      return this.graph.nodes.find((n) =&amp;gt; n.id === node.id)!;
    }
    this.ids.add(node.id);
    this.graph.nodes.push(node);
    return node;
  }

  addLink&amp;lt;T = any&amp;gt;(link: GraphLink&amp;lt;T&amp;gt;) {
    this.graph.links.push(link);
    return link;
  }

  toJSON() {
    return this.graph;
  }
}

export interface GraphNode&amp;lt;T = any&amp;gt; {
  id: string;
  name?: string;
  group?: string;
  value?: T;
}

export interface GraphLink&amp;lt;T = any&amp;gt; {
  source: string;
  target: string;
  name?: string;
  value?: T;
}

export interface GraphData&amp;lt;A = any, B = any&amp;gt; {
  name?: string;
  description?: string;
  nodes: GraphNode&amp;lt;A&amp;gt;[];
  links: GraphLink&amp;lt;B&amp;gt;[];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are creating a utility class that can generate the nodes and links while excluding duplicates and returning the graph data.&lt;/p&gt;
&lt;p&gt;Create and open the file &lt;code&gt;src/classes/context.ts&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { GraphData, GraphNode } from &amp;quot;./graph&amp;quot;;

export interface RenderContext {
  data: GraphData;
  element: HTMLElement;
  onHover: (node?: GraphNode) =&amp;gt; void;
}

export type Renderer = (context: RenderContext) =&amp;gt; void;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the context type that we will use to create the renderers and pass with the data.&lt;/p&gt;
&lt;h2&gt;2D Renderer&lt;/h2&gt;
&lt;p&gt;Create and open the file &lt;code&gt;src/renderers/mode-2d.ts&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import ForceGraph from &amp;quot;force-graph&amp;quot;;
import { RenderContext } from &amp;quot;../classes/context&amp;quot;;

export function render(context: RenderContext) {
  const graph = ForceGraph();
  const style = getComputedStyle(context.element);
  const lineColor = style.getPropertyValue(&amp;quot;--graph-line-color&amp;quot;).trim();
  const bgColor = style.getPropertyValue(&amp;quot;--graph-background-color&amp;quot;).trim();
  const fgColor = style.getPropertyValue(&amp;quot;--graph-foreground-color&amp;quot;).trim();
  const nodeColor = style.getPropertyValue(&amp;quot;--graph-node-color&amp;quot;).trim();
  graph(context.element)
    .graphData(context.data)
    .width(Number(style.width.slice(0, -2)))
    .height(Number(style.height.slice(0, -2)))
    .cooldownTicks(100)
    .backgroundColor(bgColor)
    .linkColor(() =&amp;gt; lineColor)
    .linkWidth(0.2)
    .nodeCanvasObject((node: any, ctx, globalScale) =&amp;gt; {
      // Draw a circle
      ctx.beginPath();
      const size = 5 / globalScale;
      ctx.arc(node.x, node.y, size, 0, 2 * Math.PI);
      //   ctx.fillStyle = nodeColor(node, groupColors);
      ctx.fillStyle = nodeColor;
      ctx.fill();
      ctx.lineWidth = 1 / globalScale;
      ctx.strokeStyle = lineColor;
      ctx.stroke();

      if (globalScale &amp;gt;= 4) {
        const label = node.name ?? node.id;
        const fontSize = 12 / globalScale;
        ctx.font = `${fontSize}px Sans-Serif`;
        const textWidth = ctx.measureText(label).width;
        const bckgDimensions = [textWidth, fontSize].map(
          (n) =&amp;gt; n + fontSize * 0.2
        ); // some padding

        ctx.textAlign = &amp;quot;center&amp;quot;;
        ctx.textBaseline = &amp;quot;middle&amp;quot;;
        ctx.fillStyle = fgColor;
        // Measure text
        ctx.fillText(label, node.x + size * 2 + textWidth / 2, node.y);

        node.__bckgDimensions = bckgDimensions;
      }
    })
    .onNodeHover((node: any, prev: any) =&amp;gt; {
      if (node) {
        const graphNode = context.data.nodes.find((n) =&amp;gt; n.id === node.id);
        context.onHover(graphNode);
      }
      if (prev) {
        context.onHover(undefined);
      }
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are importing the context and creating the boilerplate for the 2D renderer. When the scale is greater than 4 we draw the node name to add a little more detail.&lt;/p&gt;
&lt;p&gt;Notice that on node hover we are calling the onHover callback with the hovered node and we are using custom properties to render the colors.&lt;/p&gt;
&lt;h2&gt;3D Renderer&lt;/h2&gt;
&lt;p&gt;Create and open the file &lt;code&gt;src/renderers/mode-3d.ts&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import ForceGraph from &amp;quot;3d-force-graph&amp;quot;;
import { RenderContext } from &amp;quot;../classes/context.js&amp;quot;;

export function render(context: RenderContext) {
  const graph = ForceGraph({
    controlType: &amp;quot;trackball&amp;quot;,
    rendererConfig: { antialias: true, alpha: true },
  });
  const style = getComputedStyle(context.element);
  const lineColor = style.getPropertyValue(&amp;quot;--graph-line-color&amp;quot;).trim();
  const bgColor = style.getPropertyValue(&amp;quot;--graph-background-color&amp;quot;).trim();
  const nodeColor = style.getPropertyValue(&amp;quot;--graph-node-color&amp;quot;).trim();
  graph(context.element)
    .graphData(context.data)
    .width(Number(style.width.slice(0, -2)))
    .height(Number(style.height.slice(0, -2)))
    .showNavInfo(false)
    .linkColor(() =&amp;gt; lineColor)
    .backgroundColor(bgColor)
    .nodeThreeObject((node: any) =&amp;gt; {
      const color = node.color ?? nodeColor;
      node.color = color;
      return false as any;
    })
    .nodeThreeObjectExtend(true)
    .onNodeHover((node: any, prev: any) =&amp;gt; {
      if (node) {
        const graphNode = context.data.nodes.find((n) =&amp;gt; n.id === node.id);
        context.onHover(graphNode);
      }
      if (prev) {
        context.onHover(undefined);
      }
    })
    .cooldownTicks(100);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are almost doing the same thing as the 2D renderer but creating it with &lt;a href=&quot;https://threejs.org/&quot;&gt;Three.js&lt;/a&gt; instead.&lt;/p&gt;
&lt;h2&gt;Rendering&lt;/h2&gt;
&lt;p&gt;Now open up &lt;code&gt;src/lit-force-graph.ts&lt;/code&gt; and the imports for the renderers and graph/context classes we created:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ...
import { Renderer } from &amp;quot;./classes/context&amp;quot;;
import { GraphData, GraphNode } from &amp;quot;./classes/graph&amp;quot;;
import { render as render2D } from &amp;quot;./modes/mode-2d&amp;quot;;
import { render as render3D } from &amp;quot;./modes/mode-3d&amp;quot;;
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now add the property for the graph data and the renderers in the class:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;  @property({ type: Object }) data?: GraphData;
  @state() hovered?: GraphNode;

  renderers = new Map&amp;lt;string, Renderer&amp;gt;([
    [&amp;quot;2D&amp;quot;, render2D],
    [&amp;quot;3D&amp;quot;, render3D],
  ]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update &lt;code&gt;setData&lt;/code&gt; to render with the current renderer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;setData(data: GraphData) {
    this.data = data;
    const renderer = this.renderers.get(this.mode);
    renderer?.({
        element: this.graph,
        data,
        onHover: (node) =&amp;gt; (this.hovered = node),
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And finally update the render method to show the graph title and currently hovered node:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;render() {
    return html` &amp;lt;main
      accept=&amp;quot;application/json&amp;quot;
      @drop=&amp;quot;${this.onDrop}&amp;quot;
      @dragover=&amp;quot;${(e: Event) =&amp;gt; e.preventDefault()}&amp;quot;
    &amp;gt;
      &amp;lt;div id=&amp;quot;graph&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;div id=&amp;quot;controls&amp;quot;&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;label for=&amp;quot;render-mode&amp;quot;&amp;gt;Render mode&amp;lt;/label&amp;gt;
          &amp;lt;select id=&amp;quot;render-mode&amp;quot; @change=${this.onChangeMode}&amp;gt;
            ${Array.from(this.renderers.keys()).map((mode) =&amp;gt; {
              return html` &amp;lt;option value=&amp;quot;${mode}&amp;quot;&amp;gt;${mode}&amp;lt;/option&amp;gt; `;
            })}
          &amp;lt;/select&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div id=&amp;quot;info&amp;quot;&amp;gt;
        &amp;lt;h2 id=&amp;quot;graph-name&amp;quot;&amp;gt;${this.data?.name}&amp;lt;/h2&amp;gt;
        &amp;lt;div id=&amp;quot;graph-description&amp;quot;&amp;gt;${this?.data?.description}&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div id=&amp;quot;tooltips&amp;quot;&amp;gt;
        ${this.hovered
          ? html` &amp;lt;div class=&amp;quot;node-tooltip&amp;quot;&amp;gt;
              ${this.hovered?.name ?? this.hovered?.id}
            &amp;lt;/div&amp;gt;`
          : html``}
      &amp;lt;/div&amp;gt;
    &amp;lt;/main&amp;gt;`;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Final Code&lt;/h2&gt;
&lt;p&gt;If everything was added correctly it should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, css, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, property, query, state } from &amp;quot;lit/decorators.js&amp;quot;;
import { Renderer } from &amp;quot;./classes/context&amp;quot;;
import { GraphData, GraphNode } from &amp;quot;./classes/graph&amp;quot;;
import { render as render2D } from &amp;quot;./modes/mode-2d&amp;quot;;
import { render as render3D } from &amp;quot;./modes/mode-3d&amp;quot;;

export const tagName = &amp;quot;lit-force-graph&amp;quot;;

@customElement(tagName)
export class LitForceGraph extends LitElement {
  static styles = css`
    :host {
      background-color: var(--graph-background-color, #000011);
      color: var(--graph-foreground-color, #ffffff);
      width: var(--graph-width, 100%);
      height: var(--graph-height, 100vh);
    }

    #graph {
      width: 100%;
      height: 100%;
      width: var(--graph-width, 100%);
      height: var(--graph-height, 100vh);
    }

    #controls {
      position: absolute;
      top: 20px;
      right: 20px;
      z-index: 100 !important;
      display: flex;
      flex-direction: column;
      align-items: flex-end;
    }
    #controls div {
      padding: 5px;
    }

    #info {
      position: absolute;
      top: 10px;
      left: 10px;
      z-index: 100 !important;
      display: flex;
      flex-direction: column;
      align-items: flex-start;
    }

    #tooltips {
      position: absolute;
      bottom: 10px;
      left: 10px;
      right: 10px;
      display: flex;
      flex-direction: row;
      align-items: center;
      text-align: center;
      justify-content: center;
    }

    .node-tooltip {
      background-color: var(--graph-foreground-color, #ffffff);
      color: var(--graph-background-color, #000011);
      border-radius: 5px;
      font-size: 12px;
      padding: 5px;
      opacity: 0.67;
    }

    #graph-description {
      opacity: 0.67;
    }

    .scene-tooltip {
      color: var(--graph-foreground-color, #ffffff);
      background-color: transparent;
      display: none;
    }
  `;

  @query(&amp;quot;#graph&amp;quot;) graph!: HTMLElement;
  @property() src = &amp;quot;&amp;quot;;
  @property() mode = &amp;quot;2D&amp;quot;;
  @property({ type: Object }) data?: GraphData;
  @state() hovered?: GraphNode;

  renderers = new Map&amp;lt;string, Renderer&amp;gt;([
    [&amp;quot;2D&amp;quot;, render2D],
    [&amp;quot;3D&amp;quot;, render3D],
  ]);

  render() {
    return html` &amp;lt;main
      accept=&amp;quot;application/json&amp;quot;
      @drop=&amp;quot;${this.onDrop}&amp;quot;
      @dragover=&amp;quot;${(e: Event) =&amp;gt; e.preventDefault()}&amp;quot;
    &amp;gt;
      &amp;lt;div id=&amp;quot;graph&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;div id=&amp;quot;controls&amp;quot;&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;label for=&amp;quot;render-mode&amp;quot;&amp;gt;Render mode&amp;lt;/label&amp;gt;
          &amp;lt;select id=&amp;quot;render-mode&amp;quot; @change=${this.onChangeMode}&amp;gt;
            ${Array.from(this.renderers.keys()).map((mode) =&amp;gt; {
              return html` &amp;lt;option value=&amp;quot;${mode}&amp;quot;&amp;gt;${mode}&amp;lt;/option&amp;gt; `;
            })}
          &amp;lt;/select&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div id=&amp;quot;info&amp;quot;&amp;gt;
        &amp;lt;h2 id=&amp;quot;graph-name&amp;quot;&amp;gt;${this.data?.name}&amp;lt;/h2&amp;gt;
        &amp;lt;div id=&amp;quot;graph-description&amp;quot;&amp;gt;${this?.data?.description}&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div id=&amp;quot;tooltips&amp;quot;&amp;gt;
        ${this.hovered
          ? html` &amp;lt;div class=&amp;quot;node-tooltip&amp;quot;&amp;gt;
              ${this.hovered?.name ?? this.hovered?.id}
            &amp;lt;/div&amp;gt;`
          : html``}
      &amp;lt;/div&amp;gt;
    &amp;lt;/main&amp;gt;`;
  }

  async firstUpdated() {
    await this.refresh();
    const prefersDark = window.matchMedia(&amp;quot;(prefers-color-scheme: dark)&amp;quot;);
    prefersDark.addEventListener(&amp;quot;change&amp;quot;, () =&amp;gt; {
      this.refresh();
    });
  }

  /**
   * Set the graph data and update the renderer
   *
   * @param data Graph JSON
   */
  setData(data: GraphData) {
    this.data = data;
    const renderer = this.renderers.get(this.mode);
    renderer?.({
      element: this.graph,
      data,
      onHover: (node) =&amp;gt; (this.hovered = node),
    });
  }

  private async refresh() {
    // Get json from script tag
    const children = Array.from(this.children);
    const elem = children.find((child) =&amp;gt; child.tagName === &amp;quot;SCRIPT&amp;quot;);
    if (elem) {
      // Render from script tag contents
      if (elem.textContent) {
        const data = JSON.parse(elem.textContent);
        if (data) this.setData(data);
        // Render from script tag src
      } else if (elem.hasAttribute(&amp;quot;src&amp;quot;)) {
        const url = elem.getAttribute(&amp;quot;src&amp;quot;)!;
        const data = await fetch(url).then((res) =&amp;gt; res.json());
        if (data) this.setData(data);
      }
    } else if (this.src.length &amp;gt; 0) {
      // Render from src attribute
      const data = await fetch(this.src).then((res) =&amp;gt; res.json());
      if (data) this.setData(data);
    }
  }

  private onChangeMode(e: Event) {
    const mode = (e.target as HTMLSelectElement).value;
    this.mode = mode;
    if (!this.data) return;
    this.setData({ ...this.data! });
  }

  private onDrop(e: DragEvent) {
    e.preventDefault();
    const files = e.dataTransfer?.files;
    if (files &amp;amp;&amp;amp; files.length &amp;gt; 0) {
      const file = files[0];
      const reader = new FileReader();
      reader.onload = () =&amp;gt; {
        const json = JSON.parse(reader.result as string);
        this.data = json;
        this.setData(json);
      };
      reader.readAsText(file);
    }
    return false;
  }

  attributeChangedCallback(
    name: string,
    _old: string | null,
    value: string | null
  ): void {
    if (name === &amp;quot;src&amp;quot; &amp;amp;&amp;amp; value) {
      this.refresh();
    }
    if (name === &amp;quot;data&amp;quot; &amp;amp;&amp;amp; value) {
      this.setData(JSON.parse(value));
    }
    if (name === &amp;quot;mode&amp;quot; &amp;amp;&amp;amp; value) {
      this.mode = value;
      if (this.data) {
        this.setData({ ...this.data! });
      }
    }
    super.attributeChangedCallback(name, _old, value);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    &amp;quot;lit-force-graph&amp;quot;: LitForceGraph;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2D Light:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/graph_light_hsa07drqll.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2D Dark:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/graph_dark_s951yas96p.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3D Light:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/graph_light_3d_1n3rbzomqr.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3D Dark:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/graph_dark_3d_kckpdzz3lx.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Now you can render the complex data structures with ease using web components!&lt;/p&gt;
&lt;p&gt;If you want to learn more about building with Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The source for this example can be found &lt;a href=&quot;https://github.com/rodydavis/lit-force-graph&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Using Fastlane in Flutter and CI</title><link>https://rodydavis.com/flutter/fastlane/</link><guid isPermaLink="true">https://rodydavis.com/flutter/fastlane/</guid><description>Automate Flutter app builds and releases to Google Play and App Store using Fasthlane by initializing the tool, configuring the Fasthile, and defining build/beta actions.</description><pubDate>Mon, 20 Jan 2025 02:50:39 GMT</pubDate><content:encoded>&lt;h1&gt;Using Fastlane in Flutter and CI&lt;/h1&gt;
&lt;p&gt;Prerequisites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Understand what &lt;a href=&quot;https://fastlane.tools/&quot;&gt;Fastlane&lt;/a&gt; is and how it works&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Project builds correctly following these &lt;a href=&quot;https://flutter.dev/docs/deployment/cd&quot;&gt;docs&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Android app setup in &lt;a href=&quot;https://developer.android.com/distribute/console&quot;&gt;Google Play Console&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;iOS app setup in &lt;a href=&quot;https://appstoreconnect.apple.com/&quot;&gt;AppStore Connect&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://flutter.dev/docs/get-started/install&quot;&gt;Flutter is installed&lt;/a&gt; and your project is created&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Steps&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open your Flutter project&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run: cd ios&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run: fastlane init and follow the prompts&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Replace the Fastfile contents with this:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;#!/bin/bash

echo &amp;quot;App Release Automator by @rodydavis&amp;quot;

action=&amp;quot;$1&amp;quot;
red=`tput setaf 1`
green=`tput setaf 2`
reset=`tput sgr0`

if [ ${action} = &amp;quot;build&amp;quot; ]; then

    echo &amp;quot;${green}Generating built files.. ${reset}&amp;quot;
    flutter packages pub run build_runner clean
    flutter packages pub run build_runner build --delete-conflicting-outputs

    pub global activate pubspec_version
    git commit -a -m &amp;quot;Build $(pubver bump patch)&amp;quot;
    
    echo &amp;quot;${green}Building Project...${reset}&amp;quot;
    find . -name &amp;quot;*-e&amp;quot; -type f -delete
    flutter format .
    flutter clean

    echo &amp;quot;${green}Project Size: $(find . -name &amp;quot;*.dart&amp;quot; | xargs cat | wc -c)${reset}&amp;quot;

    echo &amp;quot;${green}Building APK...${reset}&amp;quot;
    flutter build apk

    echo &amp;quot;${green}Builing IPA..${reset}&amp;quot;
    cd ./ios &amp;amp;&amp;amp; pod install &amp;amp;&amp;amp; pod repo update &amp;amp;&amp;amp; cd ..
    flutter build ios

    git commit -a -m &amp;quot;Project Rebuilt&amp;quot;


elif [ ${action} = &amp;quot;beta&amp;quot; ]; then

    echo &amp;quot;${green}Generating built files..${reset}&amp;quot;
    flutter packages pub run build_runner clean
    flutter packages pub run build_runner build --delete-conflicting-outputs

    pub global activate pubspec_version
    git commit -a -m &amp;quot;Beta $(pubver bump patch)&amp;quot;
    
    echo &amp;quot;${green}Building Project...${reset}&amp;quot;
    find . -name &amp;quot;*-e&amp;quot; -type f -delete
    flutter format .
    flutter clean

    echo &amp;quot;${green}Project Size: $(find . -name &amp;quot;*.dart&amp;quot; | xargs cat | wc -c)${reset}&amp;quot;

    echo &amp;quot;${green}Building APK...${reset}&amp;quot;
    flutter build apk

    echo &amp;quot;${green}Sending Android to Beta...${reset}&amp;quot;
    cd ./android &amp;amp;&amp;amp; fastlane beta &amp;amp;&amp;amp; cd ..

    echo &amp;quot;${green}Builing IPA..${reset}&amp;quot;
    flutter build ios

    echo &amp;quot;${green}Sending iOS to Beta..${reset}&amp;quot;
    cd ./ios &amp;amp;&amp;amp; fastlane beta &amp;amp;&amp;amp; cd ..

    git commit -a -m &amp;quot;Sent to Beta&amp;quot;


elif [ ${action} = &amp;quot;release&amp;quot; ]; then

    echo &amp;quot;${green}Generating built files..${reset}&amp;quot;
    flutter packages pub run build_runner clean
    flutter packages pub run build_runner build --delete-conflicting-outputs

    pub global activate pubspec_version
    git commit -a -m &amp;quot;Production $(pubver bump minor)&amp;quot;

    echo &amp;quot;${green}Building Project...${reset}&amp;quot;
    find . -name &amp;quot;*-e&amp;quot; -type f -delete
    flutter format .
    flutter clean

    echo &amp;quot;${green}Project Size: $(find . -name &amp;quot;*.dart&amp;quot; | xargs cat | wc -c)${reset}&amp;quot;

    echo &amp;quot;${green}Building APK...${reset}&amp;quot;
    flutter build apk

    echo &amp;quot;${green}Sending Android to Production...${reset}&amp;quot;
    cd ./android &amp;amp;&amp;amp; fastlane release &amp;amp;&amp;amp; cd ..

    echo &amp;quot;${green}Builing IPA..${reset}&amp;quot;
    flutter build ios

    echo &amp;quot;${green}Sending iOS to Production...${reset}&amp;quot;
    cd ./ios &amp;amp;&amp;amp; fastlane release &amp;amp;&amp;amp; cd ..

    git commit -a -m &amp;quot;Sent to Production&amp;quot;

fi

echo &amp;quot;${green}Successfully completed${reset}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;
&lt;p&gt;Run: cd .. &amp;amp;&amp;amp; cd android&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run: fastlane init and follow the prompts&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Replace the Fastfile contents with this:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane

default_platform(:android)

platform :android do
  desc &amp;quot;Prepare and archive app&amp;quot;
  lane :prepare  do |options|
    #bundle_install
    Dir.chdir &amp;quot;../..&amp;quot; do
      sh(&amp;quot;flutter&amp;quot;, &amp;quot;packages&amp;quot;, &amp;quot;get&amp;quot;)
      sh(&amp;quot;flutter&amp;quot;, &amp;quot;clean&amp;quot;)
      sh(&amp;quot;flutter&amp;quot;, &amp;quot;build&amp;quot;, &amp;quot;appbundle&amp;quot;, &amp;quot;--release&amp;quot;)
    end
  end
  
  desc &amp;quot;Push a new beta build to Google Play&amp;quot;
  lane :beta do
    prepare(release: false)
    upload_to_play_store(
      track: &apos;beta&apos;,
      aab: &amp;quot;../build/app/outputs/bundle/release/app.aab&amp;quot;
    )
    add_git_tag(
      grouping: &amp;quot;fastlane-builds&amp;quot;,
      prefix: &amp;quot;v&amp;quot;,
      build_number: android_get_version_code
    )
    push_to_git_remote
  end

  desc &amp;quot;Push a new release build to the Google Play&amp;quot;
  lane :release do
    prepare(release: true)
    upload_to_play_store(
      track: &apos;production&apos;,
      aab: &amp;quot;../build/app/outputs/bundle/release/app.aab&amp;quot;
    )
    add_git_tag(
      grouping: &amp;quot;release&amp;quot;,
      prefix: &amp;quot;v&amp;quot;,
      build_number: android_get_version_name
    )
    push_to_git_remote
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;8&quot;&gt;
&lt;li&gt;
&lt;p&gt;Run: fastlane add_plugin versioning_android and enter your password if needed&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run: cd ..&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now you are ready to launch your app to beta!&lt;/p&gt;
&lt;p&gt;For ios run: cd ios &amp;amp;&amp;amp; fastlane beta&lt;/p&gt;
&lt;p&gt;For android run: cd android &amp;amp;&amp;amp; fastlane beta&lt;/p&gt;
&lt;p&gt;Stay tuned for an article soon where we use these fastlane sub folders for automating the releases on &lt;a href=&quot;https://github.com/features/actions&quot;&gt;Github Actions&lt;/a&gt; CI&lt;/p&gt;
</content:encoded></item><item><title>Flutter + Fastlane (One Click Beta)</title><link>https://rodydavis.com/flutter/one-click-release/</link><guid isPermaLink="true">https://rodydavis.com/flutter/one-click-release/</guid><description>Deploy Flutter apps to the App Store and Google Play quickly with Fastlane, a one-click beta testing solution for iOS and Android.</description><pubDate>Mon, 20 Jan 2025 00:07:34 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter + Fastlane (One Click Beta)&lt;/h1&gt;
&lt;h2&gt;1. Install Flutter&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://flutter.io/get-started/install/&quot;&gt;Download Flutter&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/ff_1_u67ip3pbk5.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;2. Create new Flutter Project&lt;/h2&gt;
&lt;p&gt;If you are pretty new to Flutter you can check out &lt;a href=&quot;https://flutter.io/get-started/codelab/&quot;&gt;this useful guide&lt;/a&gt; on how to create a new project step by step.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/ff_2_9dljl4hgqq.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3. Create App in iTunes Connect&lt;/h2&gt;
&lt;p&gt;If you are not familiar with iTunes Connect, check out &lt;a href=&quot;https://clearbridgemobile.com/how-to-submit-an-app-to-the-app-store/&quot;&gt;this article&lt;/a&gt; for getting started and setting up your first app for the App Store.&lt;/p&gt;
&lt;h2&gt;4. Create App in Google Play&lt;/h2&gt;
&lt;p&gt;Setting up an app in the Google Play Console can be tricky, make sure to check out the &lt;a href=&quot;https://support.google.com/googleplay/android-developer/answer/113469?hl=en-GB&quot;&gt;official reference&lt;/a&gt; and &lt;a href=&quot;https://medium.com/mindorks/upload-your-first-android-app-on-play-store-step-by-step-ee0de9123ac0&quot;&gt;this guide&lt;/a&gt; if you are having trouble.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/ff_3_1z55io2svg.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;5. Navigate to Project &amp;gt; ios and Setup Fastlane&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.fastlane.tools/getting-started/ios/setup/&quot;&gt;Reference&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;6. Navigate to Project &amp;gt; android and Setup Fastlane&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.fastlane.tools/getting-started/android/setup/&quot;&gt;Reference&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;7. Update Fastlane Fastfiles for iOS and Android and Change accordingly for each platform&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Make sure to change &amp;quot;YOUR PROJECT PATH&amp;quot; to the path to your project in Finder.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Only copy the correct platform code for each Fastfile. For example, &lt;code&gt;default_platform(:ios)&lt;/code&gt; for iOS and `default_platform(:android)1st for Android.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;update_fastlane

default_platform(:ios)

platform :ios do
  desc &amp;quot;Push a new beta build to TestFlight&amp;quot;
  lane :beta do
    increment_build_number(xcodeproj: &amp;quot;Runner.xcodeproj&amp;quot;)
    build_app(workspace: &amp;quot;Runner.xcworkspace&amp;quot;, scheme: &amp;quot;Runner&amp;quot;)
    upload_to_testflight(skip_waiting_for_build_processing: true)
  end
  desc &amp;quot;Push a new release build to the App Store&amp;quot;
  lane :release do  
    increment_build_number(xcodeproj: &amp;quot;Runner.xcodeproj&amp;quot;)
    build_app(workspace: &amp;quot;Runner.xcworkspace&amp;quot;, scheme: &amp;quot;Runner&amp;quot;)
    upload_to_app_store(submit_for_review: true,
                            automatic_release: true,
                            skip_screenshots: true,
                            force: true,
                            skip_waiting_for_build_processing: true)
  end
end


  
//YOUR PROJECT PATH &amp;gt; android &amp;gt; fastlane &amp;gt; Fastfile
default_platform(:android)

platform :android do
  desc &amp;quot;Runs all the tests&amp;quot;
  lane :test do
    gradle(task: &amp;quot;test&amp;quot;)
  end

  desc &amp;quot;Submit a new Build to Beta&amp;quot;
  lane :beta do
    gradle(task: &apos;clean&apos;)
    increment_version_code
    sh &amp;quot;cd YOUR PROJECT PATH &amp;amp;&amp;amp; flutter build apk&amp;quot;
    upload_to_play_store(
      track: &apos;beta&apos;,
      apk: &apos;../build/app/outputs/apk/release/app-release.apk&apos;,
      skip_upload_screenshots: true,
      skip_upload_images: true
    )
    # crashlytics
  end

  desc &amp;quot;Deploy a new version to the Google Play&amp;quot;
  lane :deploy do
    gradle(task: &apos;clean&apos;)
    increment_version_code
    sh &amp;quot;cd YOUR PROJECT PATH &amp;amp;&amp;amp; flutter build apk&amp;quot;
    upload_to_play_store(
      track: &apos;production&apos;,
      apk: &apos;../build/app/outputs/apk/release/app-release.apk&apos;,
      skip_upload_screenshots: true,
      skip_upload_images: true
    )
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;For Android &lt;code&gt;increment_version_code&lt;/code&gt; install here.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sometimes it will fail and you will need to run:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bundle exec fastlane add_plugin increment_version_code&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For iOS &lt;code&gt;increment_build_number&lt;/code&gt; set up Generic Versioning by enabling the agvtool.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/ff_4_39myyg3bzm.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/xcblog/agvtool-automating-ios-build-and-version-numbers-454cab6f1bbe&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;8. Metadata (Optional)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;For iOS you can have Fastlane download all your apps existing metadata including screenshots from iTunes Connect. In terminal navigate to the project and run.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;fastlane deliver download_metadata &amp;amp;&amp;amp; fastlane deliver download_screenshots&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For Android you can use &lt;a href=&quot;https://docs.fastlane.tools/actions/supply/&quot;&gt;Fastlane Supply&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9. Open Automator&lt;/h2&gt;
&lt;p&gt;Right now everything is working just by the command line. If you navigate to your project in terminal by adding &amp;quot;cd &amp;quot; and dragging in the project folder and hitting Enter, you can type &amp;quot;cd ios &amp;amp;&amp;amp; fastlane beta&amp;quot; or &amp;quot;cd android &amp;amp;&amp;amp; fastlane beta&amp;quot; and both will run fastlane.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/ff_5_1esbyy658n.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you want to be able to submit your app to Google Play and the App Store with one click we will be using &lt;a href=&quot;http://www.applegazette.com/os-x/getting-started-automator-workflows-mac/&quot;&gt;Automator&lt;/a&gt;. Create a new Automator Application. And Search for &amp;quot;Ask for Confirmation&amp;quot; and &amp;quot;Run AppleScript&amp;quot; and drag in.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/ff_6_krrntoi3wl.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here is the Script for beta and release. You will need to create a Automator Application for both Beta and Release for each app you want automated. Save it where ever you want and create an Alias to be but on the Desktop.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make sure to change &lt;strong&gt;YOUR PROJECT PATH&lt;/strong&gt; to the path to your project in Finder&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hint: I have my automator application save in the Github Repo of my project for versioning and easy access for different projects.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;//Beta
on run {input, parameters}
	tell application &amp;quot;Terminal&amp;quot;
		activate
		do script &amp;quot;cd YOUR PROJECT PATH/android &amp;amp;&amp;amp; fastlane beta &amp;amp;&amp;amp; cd YOUR PROJECT PATH/ios &amp;amp;&amp;amp; fastlane beta&amp;quot;
	end tell
	tell application &amp;quot;System Events&amp;quot;
		try
			set visible of application process &amp;quot;Terminal&amp;quot; to false
		end try
	end tell
end run

//Release
on run {input, parameters}
	tell application &amp;quot;Terminal&amp;quot;
		activate
		do script &amp;quot;cd YOUR PROJECT PATH/android &amp;amp;&amp;amp; fastlane deploy &amp;amp;&amp;amp; cd YOUR PROJECT PATH/ios &amp;amp;&amp;amp; fastlane release&amp;quot;
	end tell
	tell application &amp;quot;System Events&amp;quot;
		try
			set visible of application process &amp;quot;Terminal&amp;quot; to false
		end try
	end tell
end run
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. Try It Out!&lt;/h2&gt;
&lt;p&gt;Everything should be working now. If you double click on the automator application you should get a confirmation pop up to release the app. The Script will run terminal in the background and you can stay focused on developing awesome flutter applications. If you want to see the progress on fastlane uploading your apps you can click on the terminal icon and the terminal window will reappear. Thanks for reading and please reach out for any questions you have!&lt;/p&gt;
</content:encoded></item><item><title>JSON to HTML Table with Lit</title><link>https://rodydavis.com/lit/html-table/</link><guid isPermaLink="true">https://rodydavis.com/lit/html-table/</guid><description>Create a dynamic HTML table from JSON data using Lit, a web component framework, with a detailed guide and online demo.</description><pubDate>Mon, 20 Jan 2025 01:18:07 GMT</pubDate><content:encoded>&lt;h1&gt;JSON to HTML Table with Lit&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to set up a &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; web component and use it to create a HTML &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table&quot;&gt;Table&lt;/a&gt; from json url or inline json.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/lit-html-table&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/lit-html-table/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Node &amp;gt;= 16&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by navigating in terminal to the location of the project and run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init @vitejs/app --template lit-ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enter a project name &lt;code&gt;lit-html-table&lt;/code&gt; and now open the project in vscode and install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd lit-html-table
npm i lit
npm i -D @types/node
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;vite.config.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from &amp;quot;vite&amp;quot;;
import { resolve } from &amp;quot;path&amp;quot;;

export default defineConfig({
  base: &amp;quot;/lit-html-table/&amp;quot;,
  build: {
    lib: {
      entry: &amp;quot;src/lit-html-table.ts&amp;quot;,
      formats: [&amp;quot;es&amp;quot;],
    },
    rollupOptions: {
      input: {
        main: resolve(__dirname, &amp;quot;index.html&amp;quot;),
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Template&lt;/h2&gt;
&lt;p&gt;Open up the &lt;code&gt;index.html&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/src/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;JSON to Lit HTML Table&amp;lt;/title&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/lit-html-table.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;lit-html-table src=&amp;quot;https://jsonplaceholder.typicode.com/posts&amp;quot;&amp;gt;
      &amp;lt;!-- &amp;lt;span slot=&amp;quot;title&amp;quot; style=&amp;quot;color: red;&amp;quot;&amp;gt;Title&amp;lt;/span&amp;gt; --&amp;gt;
      &amp;lt;!-- &amp;lt;script type=&amp;quot;application/json&amp;quot;&amp;gt;
      [
        {
          &amp;quot;id&amp;quot;: &amp;quot;0&amp;quot;,
          &amp;quot;name&amp;quot;: &amp;quot;First Item&amp;quot;
        }
      ]
    &amp;lt;/script&amp;gt; --&amp;gt;
    &amp;lt;/lit-html-table&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are passing a src attribute to the web component for this example but we can also add a script tag with the type attribute set to &lt;code&gt;application/json&lt;/code&gt; with the contents containing the json.&lt;/p&gt;
&lt;p&gt;If any table header cell needed to be replaced an element can be provided with the slot name set to the key in the json object.&lt;/p&gt;
&lt;h2&gt;Web Component&lt;/h2&gt;
&lt;p&gt;Before we update our component we need to rename &lt;code&gt;my-element.ts&lt;/code&gt; to &lt;code&gt;lit-html-table.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open up &lt;code&gt;lit-html-table.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, css, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, property } from &amp;quot;lit/decorators.js&amp;quot;;

type ObjectData = { [key: string]: any };

@customElement(&amp;quot;lit-html-table&amp;quot;)
export class LitHtmlTable extends LitElement {
  @property() src = &amp;quot;&amp;quot;;

  data?: ObjectData[];

  static styles = css`
    tr {
      text-align: var(--table-tr-text-align, left);
      vertical-align: var(--table-tr-vertical-align, top);
      padding: var(--table-tr-padding, 10px);
    }
  `;

  render() {
    // Check if data is loaded
    if (!this.values) {
      return html`&amp;lt;slot name=&amp;quot;loading&amp;quot;&amp;gt;Loading...&amp;lt;/slot&amp;gt;`;
    }
    // Check if items are not empty
    if (this.values.length === 0) {
      return html`&amp;lt;slot name=&amp;quot;empty&amp;quot;&amp;gt;No Items Found!&amp;lt;/slot&amp;gt;`;
    }
    // Convert JSON to HTML Table
    return html`
      &amp;lt;table&amp;gt;
        &amp;lt;thead&amp;gt;
          &amp;lt;tr&amp;gt;
            ${Object.keys(this.values[0]).map((key) =&amp;gt; {
              const name = key.replace(/\b([a-z])/g, (_, val) =&amp;gt;
                val.toUpperCase()
              );
              return html`&amp;lt;th&amp;gt;
                &amp;lt;slot name=&amp;quot;${key}&amp;quot;&amp;gt;${name}&amp;lt;/slot&amp;gt;
              &amp;lt;/th&amp;gt;`;
            })}
          &amp;lt;/tr&amp;gt;
        &amp;lt;/thead&amp;gt;
        &amp;lt;tbody&amp;gt;
          ${this.values.map((item) =&amp;gt; {
            return html`
              &amp;lt;tr&amp;gt;
                ${Object.values(item).map((row) =&amp;gt; {
                  return html`&amp;lt;td&amp;gt;${row}&amp;lt;/td&amp;gt;`;
                })}
              &amp;lt;/tr&amp;gt;
            `;
          })}
        &amp;lt;/tbody&amp;gt;
      &amp;lt;/table&amp;gt;
    `;
  }

  async firstUpdated() {
    await this.fetchData();
  }

  // Download the latest json and update it locally
  async fetchData() {
    let _data: any;
    if (this.src.length &amp;gt; 0) {
      // If a src attribute is set prefer it over any slots
      _data = await fetch(this.src).then((res) =&amp;gt; res.json());
    } else {
      // If no src attribute is set then grab the inline json in the slot
      const elem = this.parentElement?.querySelector(
        &apos;script[type=&amp;quot;application/json&amp;quot;]&apos;
      ) as HTMLScriptElement;
      if (elem) _data = JSON.parse(elem.innerHTML);
    }
    this.values = this.transform(_data ?? []);
    this.requestUpdate();
  }

  transform(data: any) {
    return data;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have defined a few &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/--*&quot;&gt;CSS Custom Properties&lt;/a&gt; to style the table cell but many more can be added here.&lt;/p&gt;
&lt;p&gt;If everything goes well run the command &lt;code&gt;npm run dev&lt;/code&gt; and the follow should appear:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/h_1_dab8xdgmxb.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Editing&lt;/h2&gt;
&lt;p&gt;What if we wanted to support editing of any cell? With Lit and Web Components we can progressively enhance the experience without changing the html.&lt;/p&gt;
&lt;p&gt;At the top of the class add the following boolean property:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;@property({ type: Boolean }) editable = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now update the &lt;code&gt;tbody&lt;/code&gt; tag in the render method:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&amp;lt;tbody&amp;gt;
  ${this.values.map((item, index) =&amp;gt; {
    return html`
      &amp;lt;tr&amp;gt;
        ${Object.entries(item).map((row) =&amp;gt; {
          return html`&amp;lt;td&amp;gt;
            ${this.editable
              ? html`&amp;lt;input
                  value=&amp;quot;${row[1]}&amp;quot;
                  type=&amp;quot;text&amp;quot;
                  @input=${(e: any) =&amp;gt; {
                    const value = e.target.value;
                    const key = row[0];
                    const current = this.values![index];
                    current[key] = value;
                    this.values![index] = current;
                    this.requestUpdate();
                    this.dispatchEvent(
                      new CustomEvent(&amp;quot;input-cell&amp;quot;, {
                        detail: {
                          index: index,
                          data: current,
                        },
                      })
                    );
                  }}
                /&amp;gt;`
              : html`${row[1]}`}
          &amp;lt;/td&amp;gt;`;
        })}
      &amp;lt;/tr&amp;gt;
    `;
  })}
&amp;lt;/tbody&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By checking to see if the &lt;code&gt;editable&lt;/code&gt; and if &lt;code&gt;true&lt;/code&gt; return an input with an event listener to update the data and dispatch an &lt;code&gt;input&lt;/code&gt; event.&lt;/p&gt;
&lt;p&gt;Add the &lt;code&gt;editable&lt;/code&gt; attribute to the &lt;code&gt;index.html&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;lit-html-table editable&amp;gt; ... &amp;lt;/lit-html-table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After a reload the table should look like this and any cell can be edited.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/h_2_8s8pheteqq.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;An event listener can be added just before the closing &lt;code&gt;body&lt;/code&gt; tag in &lt;code&gt;index.html&lt;/code&gt; to grab the latest values or cell information:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;script&amp;gt;
  const elem = document.querySelector(&amp;quot;lit-html-table&amp;quot;);
  elem.addEventListener(
    &amp;quot;input-cell&amp;quot;,
    (e) =&amp;gt; {
      // Index and data for the individual cell
      const { index, data } = e.detail;
      // New array of json items
      const values = elem.values;
    },
    false
  );
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can be taken farther by checking for the type of the value and returning a color, number or checkbox input.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to learn more about building with Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;. There is also an example on the Lit playground &lt;a href=&quot;https://lit.dev/playground/#project=W3sibmFtZSI6ImxpdC1odG1sLXRhYmxlLnRzIiwiY29udGVudCI6ImltcG9ydCB7IGh0bWwsIGNzcywgTGl0RWxlbWVudCB9IGZyb20gXCJsaXRcIjtcbmltcG9ydCB7IGN1c3RvbUVsZW1lbnQsIHByb3BlcnR5IH0gZnJvbSBcImxpdC9kZWNvcmF0b3JzLmpzXCI7XG5cbnR5cGUgT2JqZWN0RGF0YSA9IHsgW2tleTogc3RyaW5nXTogYW55IH07XG5cbkBjdXN0b21FbGVtZW50KFwibGl0LWh0bWwtdGFibGVcIilcbmV4cG9ydCBjbGFzcyBMaXRIdG1sVGFibGUgZXh0ZW5kcyBMaXRFbGVtZW50IHtcbiAgQHByb3BlcnR5KCkgc3JjID0gXCJcIjtcblxuICBkYXRhPzogT2JqZWN0RGF0YVtdO1xuXG4gIHN0YXRpYyBzdHlsZXMgPSBjc3NgXG4gICAgdHIge1xuICAgICAgdGV4dC1hbGlnbjogdmFyKC0tdGFibGUtdHItdGV4dC1hbGlnbiwgbGVmdCk7XG4gICAgICB2ZXJ0aWNhbC1hbGlnbjogdmFyKC0tdGFibGUtdHItdmVydGljYWwtYWxpZ24sIHRvcCk7XG4gICAgICBwYWRkaW5nOiB2YXIoLS10YWJsZS10ci1wYWRkaW5nLCAxMHB4KTtcbiAgICB9XG4gIGA7XG5cbiAgcmVuZGVyKCkge1xuICAgIC8vIENoZWNrIGlmIGRhdGEgaXMgbG9hZGVkXG4gICAgaWYgKCF0aGlzLmRhdGEpIHtcbiAgICAgIHJldHVybiBodG1sYDxzbG90IG5hbWU9XCJsb2FkaW5nXCI-TG9hZGluZy4uLjwvc2xvdD5gO1xuICAgIH1cbiAgICAvLyBDaGVjayBpZiBpdGVtcyBhcmUgbm90IGVtcHR5XG4gICAgaWYgKHRoaXMuZGF0YS5sZW5ndGggPT09IDApIHtcbiAgICAgIHJldHVybiBodG1sYDxzbG90IG5hbWU9XCJlbXB0eVwiPk5vIEl0ZW1zIEZvdW5kITwvc2xvdD5gO1xuICAgIH1cbiAgICAvLyBDb252ZXJ0IEpTT04gdG8gSFRNTCBUYWJsZVxuICAgIHJldHVybiBodG1sYFxuICAgICAgPHRhYmxlPlxuICAgICAgICA8dGhlYWQ-XG4gICAgICAgICAgPHRyPlxuICAgICAgICAgICAgJHtPYmplY3Qua2V5cyh0aGlzLmRhdGFbMF0pLm1hcCgoa2V5KSA9PiB7XG4gICAgICAgICAgICAgIGNvbnN0IG5hbWUgPSBrZXkucmVwbGFjZSgvXFxiKFthLXpdKS9nLCAoXywgdmFsKSA9PlxuICAgICAgICAgICAgICAgIHZhbC50b1VwcGVyQ2FzZSgpXG4gICAgICAgICAgICAgICk7XG4gICAgICAgICAgICAgIHJldHVybiBodG1sYDx0aD5cbiAgICAgICAgICAgICAgICA8c2xvdCBuYW1lPVwiJHtrZXl9XCI-JHtuYW1lfTwvc2xvdD5cbiAgICAgICAgICAgICAgPC90aD5gO1xuICAgICAgICAgICAgfSl9XG4gICAgICAgICAgPC90cj5cbiAgICAgICAgPC90aGVhZD5cbiAgICAgICAgPHRib2R5PlxuICAgICAgICAgICR7dGhpcy5kYXRhLm1hcCgoaXRlbSkgPT4ge1xuICAgICAgICAgICAgcmV0dXJuIGh0bWxgXG4gICAgICAgICAgICAgIDx0cj5cbiAgICAgICAgICAgICAgICAke09iamVjdC52YWx1ZXMoaXRlbSkubWFwKCh2YWwpID0-IHtcbiAgICAgICAgICAgICAgICAgIHJldHVybiBodG1sYDx0ZD4ke3ZhbH08L3RkPmA7XG4gICAgICAgICAgICAgICAgfSl9XG4gICAgICAgICAgICAgIDwvdHI-XG4gICAgICAgICAgICBgO1xuICAgICAgICAgIH0pfVxuICAgICAgICA8L3Rib2R5PlxuICAgICAgPC90YWJsZT5cbiAgICBgO1xuICB9XG5cbiAgYXN5bmMgZmlyc3RVcGRhdGVkKCkge1xuICAgIGF3YWl0IHRoaXMuZmV0Y2hEYXRhKCk7XG4gIH1cblxuICBhc3luYyBmZXRjaERhdGEoKSB7XG4gICAgbGV0IF9kYXRhOiBhbnk7XG4gICAgaWYgKHRoaXMuc3JjLmxlbmd0aCA-IDApIHtcbiAgICAgIF9kYXRhID0gYXdhaXQgZmV0Y2godGhpcy5zcmMpLnRoZW4oKHJlcykgPT4gcmVzLmpzb24oKSk7XG4gICAgfSBlbHNlIHtcbiAgICAgIGNvbnN0IGVsZW0gPSB0aGlzLnBhcmVudEVsZW1lbnQ_LnF1ZXJ5U2VsZWN0b3IoXG4gICAgICAgICdzY3JpcHRbdHlwZT1cImFwcGxpY2F0aW9uL2pzb25cIl0nXG4gICAgICApIGFzIEhUTUxTY3JpcHRFbGVtZW50O1xuICAgICAgaWYgKGVsZW0pIF9kYXRhID0gSlNPTi5wYXJzZShlbGVtLmlubmVySFRNTCk7XG4gICAgfVxuICAgIF9kYXRhID8_PSBbXTtcbiAgICB0aGlzLmRhdGEgPSB0aGlzLnRyYW5zZm9ybShfZGF0YSk7XG4gICAgdGhpcy5yZXF1ZXN0VXBkYXRlKCk7XG4gIH1cblxuICB0cmFuc2Zvcm0oZGF0YTogYW55KSB7XG4gICAgcmV0dXJuIGRhdGE7XG4gIH1cbn1cbiJ9LHsibmFtZSI6ImluZGV4Lmh0bWwiLCJjb250ZW50IjoiPCFET0NUWVBFIGh0bWw-XG48aHRtbCBsYW5nPVwiZW5cIj5cblxuPGhlYWQ-XG4gIDxtZXRhIGNoYXJzZXQ9XCJVVEYtOFwiIC8-XG4gIDxsaW5rIHJlbD1cImljb25cIiB0eXBlPVwiaW1hZ2Uvc3ZnK3htbFwiIGhyZWY9XCIvc3JjL2Zhdmljb24uc3ZnXCIgLz5cbiAgPG1ldGEgbmFtZT1cInZpZXdwb3J0XCIgY29udGVudD1cIndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjBcIiAvPlxuICA8dGl0bGU-SlNPTiB0byBMaXQgSFRNTCBUYWJsZTwvdGl0bGU-XG4gIDxzY3JpcHQgdHlwZT1cIm1vZHVsZVwiIHNyYz1cIi4vbGl0LWh0bWwtdGFibGUuanNcIj48L3NjcmlwdD5cbjwvaGVhZD5cblxuPGJvZHk-XG4gIDxsaXQtaHRtbC10YWJsZSBzcmM9XCJodHRwczovL2pzb25wbGFjZWhvbGRlci50eXBpY29kZS5jb20vcG9zdHNcIj5cbiAgICA8IS0tIDxzcGFuIHNsb3Q9XCJ0aXRsZVwiIHN0eWxlPVwiY29sb3I6IHJlZDtcIj5UaXRsZTwvc3Bhbj4gLS0-XG4gICAgPCEtLSA8c2NyaXB0IHR5cGU9XCJhcHBsaWNhdGlvbi9qc29uXCI-XG4gICAgICBbXG4gICAgICAgIHtcbiAgICAgICAgICBcImlkXCI6IFwiMFwiLFxuICAgICAgICAgIFwibmFtZVwiOiBcIkZpcnN0IEl0ZW1cIlxuICAgICAgICB9XG4gICAgICBdXG4gICAgPC9zY3JpcHQ-IC0tPlxuICA8L2xpdC1odG1sLXRhYmxlPlxuXG48L2JvZHk-XG5cbjwvaHRtbD4ifV0&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The source for this example can be found &lt;a href=&quot;https://github.com/rodydavis/lit-html-table&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Building A Piano with Flutter</title><link>https://rodydavis.com/lit/making-a-piano/</link><guid isPermaLink="true">https://rodydavis.com/lit/making-a-piano/</guid><description>Create a piano app with only 5032 bytes of Dart code using Flutter, requiring a SoundFont file and a physical device, with setup instructions and code dependencies provided.</description><pubDate>Sun, 19 Jan 2025 06:48:38 GMT</pubDate><content:encoded>&lt;h1&gt;Building A Piano with Flutter&lt;/h1&gt;
&lt;p&gt;This piano uses only &lt;code&gt;5032&lt;/code&gt; bytes of Dart Code!&lt;/p&gt;
&lt;p&gt;Winner of the &lt;a href=&quot;https://flutter.dev/create&quot;&gt;Flutter Create Contest&lt;/a&gt; and you can see the certificate &lt;a href=&quot;https://www.credential.net/exbvca0q?key=8be94f32ad2f56882045e013e960fa888afa4edd52edb963c48df351c7d1e443&quot;&gt;here&lt;/a&gt;!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/flutter_piano/tree/5k&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://pocketpiano.app/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://flutter.dev/docs/get-started/install&quot;&gt;Flutter SDK&lt;/a&gt; Installed (&lt;a href=&quot;https://flutter.dev/docs/get-started/codelab&quot;&gt;More Info&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;.sf2&lt;/code&gt; SoundFont File like &lt;a href=&quot;https://github.com/rodydavis/flutter_piano/blob/5k/assets/sounds/Piano.sf2&quot;&gt;this one&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Physical iOS device (iOS Simulator does not work with this plugin for playing the sounds) or Android Emulator/Device&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting Up&lt;/h2&gt;
&lt;p&gt;You can either create a new project with Android Studio or VSCode using the GUI or navigate to the location you want your project and using this command in the terminal: &lt;code&gt;flutter create -i swift -a kotlin flutter_piano&lt;/code&gt;. Make sure to include Swift and Kotlin Support!&lt;/p&gt;
&lt;p&gt;Now that you have your project created it should look like this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/piano_1_0o63uvabr3.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Let&apos;s start by adding some dependencies to our &lt;code&gt;pubspec.yaml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;dependencies:
  flutter:
    sdk: flutter
  tonic: ^0.2.3
  flutter_midi: ^0.1.1+3
  cupertino_icons: ^0.1.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and add the .sf2 file&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt; assets:
   - assets/sounds/Piano.sf2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you haven&apos;t already create a new folder at the top of your project call &lt;code&gt;assets&lt;/code&gt; and a subfolder called &lt;code&gt;sounds&lt;/code&gt; and place the .sf2 file there and make sure it is named &lt;code&gt;Piano.sf2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Because our app will only work in landscape we need to update those settings as well.&lt;/p&gt;
&lt;p&gt;navigate to the &lt;code&gt;/android/app/src/main/AndroidManifest.xml&lt;/code&gt; and add this line inside &lt;code&gt;&amp;lt;activity&lt;/code&gt; in the &lt;code&gt;&amp;lt;application&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;android:screenOrientation=&amp;quot;landscape&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;manifest xmlns:android=&amp;quot;http://schemas.android.com/apk/res/android&amp;quot;
    package=&amp;quot;com.rodydavis.flutter_piano&amp;quot;&amp;gt;

    &amp;lt;!-- io.flutter.app.FlutterApplication is an android.app.Application that
         calls FlutterMain.startInitialization(this); in its onCreate method.
         In most cases you can leave this as-is, but you if you want to provide
         additional functionality it is fine to subclass or reimplement
         FlutterApplication and put your custom class here. --&amp;gt;
    &amp;lt;application
        android:name=&amp;quot;io.flutter.app.FlutterApplication&amp;quot;
        android:label=&amp;quot;flutter_piano&amp;quot;
        android:icon=&amp;quot;@mipmap/ic_launcher&amp;quot;&amp;gt;
        &amp;lt;activity
            android:name=&amp;quot;.MainActivity&amp;quot;
            android:launchMode=&amp;quot;singleTop&amp;quot;
            android:theme=&amp;quot;@style/LaunchTheme&amp;quot;
            android:configChanges=&amp;quot;orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode&amp;quot;
            android:hardwareAccelerated=&amp;quot;true&amp;quot;
            android:screenOrientation=&amp;quot;landscape&amp;quot;
            android:windowSoftInputMode=&amp;quot;adjustResize&amp;quot;&amp;gt;
            &amp;lt;!-- This keeps the window background of the activity showing
                 until Flutter renders its first frame. It can be removed if
                 there is no splash screen (such as the default splash screen
                 defined in @style/LaunchTheme). --&amp;gt;
            &amp;lt;meta-data
                android:name=&amp;quot;io.flutter.app.android.SplashScreenUntilFirstFrame&amp;quot;
                android:value=&amp;quot;true&amp;quot; /&amp;gt;
            &amp;lt;intent-filter&amp;gt;
                &amp;lt;action android:name=&amp;quot;android.intent.action.MAIN&amp;quot;/&amp;gt;
                &amp;lt;category android:name=&amp;quot;android.intent.category.LAUNCHER&amp;quot;/&amp;gt;
            &amp;lt;/intent-filter&amp;gt;
        &amp;lt;/activity&amp;gt;
    &amp;lt;/application&amp;gt;
&amp;lt;/manifest&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Navigate to &lt;code&gt;/ios/Runner/info.plist&lt;/code&gt; and change:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;key&amp;gt;UISupportedInterfaceOrientations&amp;lt;/key&amp;gt;
&amp;lt;array&amp;gt;
    &amp;lt;string&amp;gt;UIInterfaceOrientationLandscapeLeft&amp;lt;/string&amp;gt;
    &amp;lt;string&amp;gt;UIInterfaceOrientationLandscapeRight&amp;lt;/string&amp;gt;
&amp;lt;/array&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can start with the UI! When you run the application now it should start in landscape!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/piano_2_mhactwtphn.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Step 1&lt;/h2&gt;
&lt;p&gt;To make it eaiser to read lets remove the comments. Use &amp;quot;find and replace&amp;quot; and search for &lt;code&gt;\/\/.*&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;&lt;img src=&quot;../../../assets/piano_3_46kmsg9amc.webp&quot; alt=&quot;&quot;&gt;&lt;/h2&gt;
&lt;p&gt;Choose the &amp;quot;select all occurrances&amp;quot; button and hit &lt;code&gt;backspace&lt;/code&gt; to delete.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/piano_4_de46u35luo.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Hit save and you should see the code format for you.&lt;/p&gt;
&lt;p&gt;The &apos;main.dart&apos; file should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: &apos;Flutter Demo Home Page&apos;),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() =&amp;gt; _MyHomePageState();
}

class _MyHomePageState extends State&amp;lt;MyHomePage&amp;gt; {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &amp;lt;Widget&amp;gt;[
            Text(
              &apos;You have pushed the button this many times:&apos;,
            ),
            Text(
              &apos;$_counter&apos;,
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: &apos;Increment&apos;,
        child: Icon(Icons.add),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2&lt;/h2&gt;
&lt;p&gt;Delete the &lt;code&gt;MyHomePage&lt;/code&gt; widget so you are left with this.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: &apos;Flutter Demo Home Page&apos;),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should get an error and thats ok, we will fix that next.&lt;/p&gt;
&lt;p&gt;Replace &lt;code&gt;MyHomePage(title: &apos;Flutter Demo Home Page&apos;)&lt;/code&gt; with a &lt;code&gt;Scaffold()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3&lt;/h2&gt;
&lt;p&gt;Change &lt;code&gt;MyApp&lt;/code&gt; to a &lt;code&gt;StatefulWidget&lt;/code&gt;. You can do this quickly by selecting &lt;code&gt;MyApp&lt;/code&gt; and choose &amp;quot;Convert to StatefulWidget&amp;quot; with the helper.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/piano_5_jila6g9dng.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;It should look like this now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() =&amp;gt; _MyAppState();
}

class _MyAppState extends State&amp;lt;MyApp&amp;gt; {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Change the theme to dark. You can do this by setting the &lt;code&gt;ThemeData&lt;/code&gt; in &lt;code&gt;MaterialApp&lt;/code&gt; change&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;theme: ThemeData(
    primarySwatch: Colors.blue,
),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to this&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;theme: ThemeData.dark(),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and and &lt;code&gt;AppBar&lt;/code&gt; to the &lt;code&gt;Scaffold&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;appBar: AppBar(title: Text(&amp;quot;Flutter Piano&amp;quot;)),&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() =&amp;gt; _MyAppState();
}

class _MyAppState extends State&amp;lt;MyApp&amp;gt; {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData.dark(),
      home: Scaffold(
        appBar: AppBar(title: Text(&amp;quot;Flutter Piano&amp;quot;)),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now build and run your app, it should look like this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/piano_6_keahhzsyf2.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Step 5&lt;/h2&gt;
&lt;p&gt;We need to add some imports to the top:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/services.dart&apos;;
import &apos;package:flutter_midi/flutter_midi.dart&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you get an error make sure they are added in the &lt;code&gt;pubspec.yaml&lt;/code&gt; from earlier, then restart the app. Be sure to run &lt;code&gt;flutter packages get&lt;/code&gt; everytime you add a dependency.&lt;/p&gt;
&lt;p&gt;Now we can add out &lt;code&gt;initState()&lt;/code&gt; to our app.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt; @override
 initState() {
   FlutterMidi.unmute();
   rootBundle.load(&amp;quot;assets/sounds/Piano.sf2&amp;quot;).then((sf2) {
     FlutterMidi.prepare(sf2: sf2, name: &amp;quot;Piano.sf2&amp;quot;);
   });
   super.initState();
 }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run the app and make sure you do not get any errors. If you are running this on the iOS Simulator you will get the following error:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Could Not Load Midi on this Device. (Cannot run on simulator), have you included the sound font?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It is ok for developing the UI but once we start with the midi you will need to plug in a real device.&lt;/p&gt;
&lt;p&gt;Your code so far should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter/services.dart&apos;;
import &apos;package:flutter_midi/flutter_midi.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() =&amp;gt; _MyAppState();
}

class _MyAppState extends State&amp;lt;MyApp&amp;gt; {
  @override
  initState() {
    FlutterMidi.unmute();
    rootBundle.load(&amp;quot;assets/sounds/Piano.sf2&amp;quot;).then((sf2) {
      FlutterMidi.prepare(sf2: sf2, name: &amp;quot;Piano.sf2&amp;quot;);
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData.dark(),
      home: Scaffold(
        appBar: AppBar(title: Text(&amp;quot;Flutter Piano&amp;quot;)),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 6&lt;/h2&gt;
&lt;p&gt;To make Flutter development faster we start with containers and colors so we can make sure everything is the right size.&lt;/p&gt;
&lt;p&gt;Lets start by adding a &lt;code&gt;Drawer&lt;/code&gt; with a &lt;code&gt;ListView&lt;/code&gt; to our &lt;code&gt;Scaffold&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt; home: Scaffold(
        appBar: AppBar(title: Text(&amp;quot;Flutter Piano&amp;quot;)),
        drawer: Drawer(child: SafeArea(child: ListView(children: &amp;lt;Widget&amp;gt;[]))),
      ),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should now get a menu icon that when you press looks like this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/piano_7_emhzhly4r3.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now lets add a ListView that scrolls Horizontially to the body of the &lt;code&gt;Scaffold&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt; body: ListView.builder(
            itemCount: 7,
            scrollDirection: Axis.horizontal,
            itemBuilder: (BuildContext context, int index) {
              return Container();
            },
          )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need 7 &lt;code&gt;itemCount&lt;/code&gt; for 7 octaves on the Piano.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter/services.dart&apos;;
import &apos;package:flutter_midi/flutter_midi.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() =&amp;gt; _MyAppState();
}

class _MyAppState extends State&amp;lt;MyApp&amp;gt; {
  @override
  initState() {
    FlutterMidi.unmute();
    rootBundle.load(&amp;quot;assets/sounds/Piano.sf2&amp;quot;).then((sf2) {
      FlutterMidi.prepare(sf2: sf2, name: &amp;quot;Piano.sf2&amp;quot;);
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData.dark(),
      home: Scaffold(
          appBar: AppBar(title: Text(&amp;quot;Flutter Piano&amp;quot;)),
          drawer:
              Drawer(child: SafeArea(child: ListView(children: &amp;lt;Widget&amp;gt;[]))),
          body: ListView.builder(
            itemCount: 7,
            scrollDirection: Axis.horizontal,
            itemBuilder: (BuildContext context, int index) {
              return Container();
            },
          )),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 7&lt;/h2&gt;
&lt;p&gt;Now we need to build the octave section that will be repeated. Since every octave is identical we can repeat the octaves with minor adjustments.&lt;/p&gt;
&lt;p&gt;Lets add some parameters for use to define for our UI. Add these underneath the initState function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;double get keyWidth =&amp;gt; 80 + (80 * _widthRatio);
double _widthRatio = 0.0;
bool _showLabels = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We will use these to dynamily update the keys.&lt;/p&gt;
&lt;p&gt;Under the &lt;code&gt;itemBuilder&lt;/code&gt; lets define which octave we are working with by adding:&lt;br&gt;
&lt;code&gt;final int i = index * 12;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Our code should look like this now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter/services.dart&apos;;
import &apos;package:flutter_midi/flutter_midi.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() =&amp;gt; _MyAppState();
}

class _MyAppState extends State&amp;lt;MyApp&amp;gt; {
  @override
  initState() {
    FlutterMidi.unmute();
    rootBundle.load(&amp;quot;assets/sounds/Piano.sf2&amp;quot;).then((sf2) {
      FlutterMidi.prepare(sf2: sf2, name: &amp;quot;Piano.sf2&amp;quot;);
    });
    super.initState();
  }
  
  double get keyWidth =&amp;gt; 80 + (80 * _widthRatio);
  double _widthRatio = 0.0;
  bool _showLabels = true;
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData.dark(),
      home: Scaffold(
          appBar: AppBar(title: Text(&amp;quot;Flutter Piano&amp;quot;)),
          drawer:
              Drawer(child: SafeArea(child: ListView(children: &amp;lt;Widget&amp;gt;[]))),
          body: ListView.builder(
            itemCount: 7,
            scrollDirection: Axis.horizontal,
            itemBuilder: (BuildContext context, int index) {
              final int i = index * 12;
              return Container();
            },
          )),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 8&lt;/h2&gt;
&lt;p&gt;Now we need to add a &lt;code&gt;Stack&lt;/code&gt; for our octave:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt; return SafeArea(
               child: Stack(children: &amp;lt;Widget&amp;gt;[
                 Row(mainAxisSize: MainAxisSize.min, children: &amp;lt;Widget&amp;gt;[
                   _buildKey(24 + i, false),
                   _buildKey(26 + i, false),
                   _buildKey(28 + i, false),
                   _buildKey(29 + i, false),
                   _buildKey(31 + i, false),
                   _buildKey(33 + i, false),
                   _buildKey(35 + i, false),
                 ]),
                 Positioned(
                     left: 0.0,
                     right: 0.0,
                     bottom: 100,
                     top: 0.0,
                     child: Row(
                         mainAxisAlignment: MainAxisAlignment.spaceBetween,
                         mainAxisSize: MainAxisSize.min,
                         children: &amp;lt;Widget&amp;gt;[
                           Container(width: keyWidth * .5),
                           _buildKey(25 + i, true),
                           _buildKey(27 + i, true),
                           Container(width: keyWidth),
                           _buildKey(30 + i, true),
                           _buildKey(32 + i, true),
                           _buildKey(34 + i, true),
                           Container(width: keyWidth * .5),
                         ])),
               ]),
             );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we have defined which midi notes are played for each octave.&lt;/p&gt;
&lt;p&gt;Now add the function &lt;code&gt;_buildKey&lt;/code&gt; underneath our &lt;code&gt;build&lt;/code&gt; function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt; Widget _buildKey(int midi, bool accidental) {
     if (accidental) {
      return Container(
          width: keyWidth,
          color: Colors.black,
          margin: EdgeInsets.symmetric(horizontal: 2.0),
          padding: EdgeInsets.symmetric(horizontal: keyWidth * .1),
          child: Material(
            elevation: 6.0,
            borderRadius: borderRadius,
            shadowColor: Color(0x802196F3),
          ));
    }
    return Container(
        width: keyWidth,
        color: Colors.white,
        margin: EdgeInsets.symmetric(horizontal: 2.0));
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also add &lt;code&gt;borderRadius&lt;/code&gt; to the bottom of &lt;code&gt;main.dart&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;const BorderRadiusGeometry borderRadius = BorderRadius.only(
    bottomLeft: Radius.circular(10.0), bottomRight: Radius.circular(10.0));

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your app should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/piano_8_jie1vsmdtx.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Your code should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter/services.dart&apos;;
import &apos;package:flutter_midi/flutter_midi.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() =&amp;gt; _MyAppState();
}

class _MyAppState extends State&amp;lt;MyApp&amp;gt; {
  @override
  initState() {
    FlutterMidi.unmute();
    rootBundle.load(&amp;quot;assets/sounds/Piano.sf2&amp;quot;).then((sf2) {
      FlutterMidi.prepare(sf2: sf2, name: &amp;quot;Piano.sf2&amp;quot;);
    });
    super.initState();
  }

  double get keyWidth =&amp;gt; 80 + (80 * _widthRatio);
  double _widthRatio = 0.0;
  bool _showLabels = true;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData.dark(),
      home: Scaffold(
          appBar: AppBar(title: Text(&amp;quot;Flutter Piano&amp;quot;)),
          drawer:
              Drawer(child: SafeArea(child: ListView(children: &amp;lt;Widget&amp;gt;[]))),
          body: ListView.builder(
            itemCount: 7,
            scrollDirection: Axis.horizontal,
            itemBuilder: (BuildContext context, int index) {
              final int i = index * 12;
              return SafeArea(
                child: Stack(children: &amp;lt;Widget&amp;gt;[
                  Row(mainAxisSize: MainAxisSize.min, children: &amp;lt;Widget&amp;gt;[
                    _buildKey(24 + i, false),
                    _buildKey(26 + i, false),
                    _buildKey(28 + i, false),
                    _buildKey(29 + i, false),
                    _buildKey(31 + i, false),
                    _buildKey(33 + i, false),
                    _buildKey(35 + i, false),
                  ]),
                  Positioned(
                      left: 0.0,
                      right: 0.0,
                      bottom: 100,
                      top: 0.0,
                      child: Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          mainAxisSize: MainAxisSize.min,
                          children: &amp;lt;Widget&amp;gt;[
                            Container(width: keyWidth * .5),
                            _buildKey(25 + i, true),
                            _buildKey(27 + i, true),
                            Container(width: keyWidth),
                            _buildKey(30 + i, true),
                            _buildKey(32 + i, true),
                            _buildKey(34 + i, true),
                            Container(width: keyWidth * .5),
                          ])),
                ]),
              );
            },
          )),
    );
  }

  Widget _buildKey(int midi, bool accidental) {
    if (accidental) {
      return Container(
          width: keyWidth,
          color: Colors.black,
          margin: EdgeInsets.symmetric(horizontal: 2.0),
          padding: EdgeInsets.symmetric(horizontal: keyWidth * .1),
          child: Material(
            elevation: 6.0,
            borderRadius: borderRadius,
            shadowColor: Color(0x802196F3),
          ));
    }
    return Container(
        width: keyWidth,
        color: Colors.white,
        margin: EdgeInsets.symmetric(horizontal: 2.0));
  }
}

const BorderRadiusGeometry borderRadius = BorderRadius.only(
    bottomLeft: Radius.circular(10.0), bottomRight: Radius.circular(10.0));

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 9&lt;/h2&gt;
&lt;p&gt;Time to add midi by adding the following import to the top of the file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:tonic/tonic.dart&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &lt;code&gt;-buildKey&lt;/code&gt; function you can add this line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt; final pitchName = Pitch.fromMidiNumber(midi).toString();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can also create the piano key itself underneath it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt; final pianoKey = Stack(
     children: &amp;lt;Widget&amp;gt;[
       Semantics(
           button: true,
           hint: pitchName,
           child: Material(
               borderRadius: borderRadius,
               color: accidental ? Colors.black : Colors.white,
               child: InkWell(
                 borderRadius: borderRadius,
                 highlightColor: Colors.grey,
                 onTap: () {},
                 onTapDown: (_) =&amp;gt; FlutterMidi.playMidiNote(midi: midi),
               ))),
       Positioned(
           left: 0.0,
           right: 0.0,
           bottom: 20.0,
           child: _showLabels
               ? Text(pitchName,
                   textAlign: TextAlign.center,
                   style: TextStyle(
                       color: !accidental ? Colors.black : Colors.white))
               : Container()),
     ],
   );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remove the color from the container and replace it with &lt;code&gt;child: pianoKey,&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt; if (accidental) {
     return Container(
         width: keyWidth,
         margin: EdgeInsets.symmetric(horizontal: 2.0),
         padding: EdgeInsets.symmetric(horizontal: keyWidth * .1),
         child: Material(
             elevation: 6.0,
             borderRadius: borderRadius,
             shadowColor: Color(0x802196F3),
             child: pianoKey));
   }
   return Container(
       width: keyWidth,
       child: pianoKey,
       margin: EdgeInsets.symmetric(horizontal: 2.0));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The complete function should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt; Widget _buildKey(int midi, bool accidental) {
   final pitchName = Pitch.fromMidiNumber(midi).toString();
   final pianoKey = Stack(
     children: &amp;lt;Widget&amp;gt;[
       Semantics(
           button: true,
           hint: pitchName,
           child: Material(
               borderRadius: borderRadius,
               color: accidental ? Colors.black : Colors.white,
               child: InkWell(
                 borderRadius: borderRadius,
                 highlightColor: Colors.grey,
                 onTap: () {},
                 onTapDown: (_) =&amp;gt; FlutterMidi.playMidiNote(midi: midi),
               ))),
       Positioned(
           left: 0.0,
           right: 0.0,
           bottom: 20.0,
           child: _showLabels
               ? Text(pitchName,
                   textAlign: TextAlign.center,
                   style: TextStyle(
                       color: !accidental ? Colors.black : Colors.white))
               : Container()),
     ],
   );
   if (accidental) {
     return Container(
         width: keyWidth,
         margin: EdgeInsets.symmetric(horizontal: 2.0),
         padding: EdgeInsets.symmetric(horizontal: keyWidth * .1),
         child: Material(
             elevation: 6.0,
             borderRadius: borderRadius,
             shadowColor: Color(0x802196F3),
             child: pianoKey));
   }
   return Container(
       width: keyWidth,
       child: pianoKey,
       margin: EdgeInsets.symmetric(horizontal: 2.0));
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when you run the app it should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/piano_9_bvdvxhujh2.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Almost there! Now let&apos;s give our user some control.&lt;/p&gt;
&lt;h2&gt;Step 10&lt;/h2&gt;
&lt;p&gt;Add these settings to the &lt;code&gt;Drawer&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;Container(height: 20.0),
ListTile(title: Text(&amp;quot;Change Width&amp;quot;)),
Slider(
    activeColor: Colors.redAccent,
    inactiveColor: Colors.white,
    min: 0.0,
    max: 1.0,
    value: _widthRatio,
    onChanged: (double value) =&amp;gt;
        setState(() =&amp;gt; _widthRatio = value)),
Divider(),
ListTile(
    title: Text(&amp;quot;Show Labels&amp;quot;),
    trailing: Switch(
    value: _showLabels,
    onChanged: (bool value) =&amp;gt;
        setState(() =&amp;gt; _showLabels = value))),
Divider(),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you should see this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/piano_10_6ib9jspceh.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Step 11&lt;/h2&gt;
&lt;p&gt;To start with &lt;code&gt;Middle C&lt;/code&gt; lets add an inital scroll offset to the &lt;code&gt;ListView&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;controller: ScrollController(initialScrollOffset: 1500.0),&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Now when we start the app it should co to C4.&lt;/p&gt;
&lt;p&gt;The final App should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/piano_11_w2jua2mews.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The final code should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter/services.dart&apos;;
import &apos;package:flutter_midi/flutter_midi.dart&apos;;
import &apos;package:tonic/tonic.dart&apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() =&amp;gt; _MyAppState();
}

class _MyAppState extends State&amp;lt;MyApp&amp;gt; {
  @override
  initState() {
    FlutterMidi.unmute();
    rootBundle.load(&amp;quot;assets/sounds/Piano.sf2&amp;quot;).then((sf2) {
      FlutterMidi.prepare(sf2: sf2, name: &amp;quot;Piano.sf2&amp;quot;);
    });
    super.initState();
  }

  double get keyWidth =&amp;gt; 80 + (80 * _widthRatio);
  double _widthRatio = 0.0;
  bool _showLabels = true;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData.dark(),
      home: Scaffold(
          appBar: AppBar(title: Text(&amp;quot;Flutter Piano&amp;quot;)),
          drawer: Drawer(
              child: SafeArea(
                  child: ListView(children: &amp;lt;Widget&amp;gt;[
            Container(height: 20.0),
            ListTile(title: Text(&amp;quot;Change Width&amp;quot;)),
            Slider(
                activeColor: Colors.redAccent,
                inactiveColor: Colors.white,
                min: 0.0,
                max: 1.0,
                value: _widthRatio,
                onChanged: (double value) =&amp;gt;
                    setState(() =&amp;gt; _widthRatio = value)),
            Divider(),
            ListTile(
                title: Text(&amp;quot;Show Labels&amp;quot;),
                trailing: Switch(
                    value: _showLabels,
                    onChanged: (bool value) =&amp;gt;
                        setState(() =&amp;gt; _showLabels = value))),
            Divider(),
          ]))),
          body: ListView.builder(
            itemCount: 7,
            scrollDirection: Axis.horizontal,
            controller: ScrollController(initialScrollOffset: 1500.0),
            itemBuilder: (BuildContext context, int index) {
              final int i = index * 12;
              return SafeArea(
                child: Stack(children: &amp;lt;Widget&amp;gt;[
                  Row(mainAxisSize: MainAxisSize.min, children: &amp;lt;Widget&amp;gt;[
                    _buildKey(24 + i, false),
                    _buildKey(26 + i, false),
                    _buildKey(28 + i, false),
                    _buildKey(29 + i, false),
                    _buildKey(31 + i, false),
                    _buildKey(33 + i, false),
                    _buildKey(35 + i, false),
                  ]),
                  Positioned(
                      left: 0.0,
                      right: 0.0,
                      bottom: 100,
                      top: 0.0,
                      child: Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          mainAxisSize: MainAxisSize.min,
                          children: &amp;lt;Widget&amp;gt;[
                            Container(width: keyWidth * .5),
                            _buildKey(25 + i, true),
                            _buildKey(27 + i, true),
                            Container(width: keyWidth),
                            _buildKey(30 + i, true),
                            _buildKey(32 + i, true),
                            _buildKey(34 + i, true),
                            Container(width: keyWidth * .5),
                          ])),
                ]),
              );
            },
          )),
    );
  }

  Widget _buildKey(int midi, bool accidental) {
    final pitchName = Pitch.fromMidiNumber(midi).toString();
    final pianoKey = Stack(
      children: &amp;lt;Widget&amp;gt;[
        Semantics(
            button: true,
            hint: pitchName,
            child: Material(
                borderRadius: borderRadius,
                color: accidental ? Colors.black : Colors.white,
                child: InkWell(
                  borderRadius: borderRadius,
                  highlightColor: Colors.grey,
                  onTap: () {},
                  onTapDown: (_) =&amp;gt; FlutterMidi.playMidiNote(midi: midi),
                ))),
        Positioned(
            left: 0.0,
            right: 0.0,
            bottom: 20.0,
            child: _showLabels
                ? Text(pitchName,
                    textAlign: TextAlign.center,
                    style: TextStyle(
                        color: !accidental ? Colors.black : Colors.white))
                : Container()),
      ],
    );
    if (accidental) {
      return Container(
          width: keyWidth,
          margin: EdgeInsets.symmetric(horizontal: 2.0),
          padding: EdgeInsets.symmetric(horizontal: keyWidth * .1),
          child: Material(
              elevation: 6.0,
              borderRadius: borderRadius,
              shadowColor: Color(0x802196F3),
              child: pianoKey));
    }
    return Container(
        width: keyWidth,
        child: pianoKey,
        margin: EdgeInsets.symmetric(horizontal: 2.0));
  }
}

const BorderRadiusGeometry borderRadius = BorderRadius.only(
    bottomLeft: Radius.circular(10.0), bottomRight: Radius.circular(10.0));

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;If you delete &lt;code&gt;tests/&lt;/code&gt; and run &lt;code&gt;find . -name &amp;quot;*.dart&amp;quot; | xargs cat | wc -c&lt;/code&gt; you will see that the dart code only uses &lt;code&gt;5032&lt;/code&gt; bytes of space!&lt;/p&gt;
&lt;p&gt;Now we have a fully functional piano that you can play with and enjoy on iOS and Android.&lt;/p&gt;
&lt;p&gt;I was really inspired when creating this for the &lt;a href=&quot;https://flutter.dev/create&quot;&gt;Flutter Create&lt;/a&gt; contest.&lt;/p&gt;
&lt;p&gt;Hope you learned something, if you have any questions you can always read out to me. This is an open source piano and would love PRs on the main project &lt;a href=&quot;https://github.com/rodydavis/flutter_piano&quot;&gt;here&lt;/a&gt;!&lt;/p&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
</content:encoded></item><item><title>Lit and Monaco Editor</title><link>https://rodydavis.com/lit/monaco-editor/</link><guid isPermaLink="true">https://rodydavis.com/lit/monaco-editor/</guid><description>This tutorial demonstrates how to create a web component using Lit and integrate the Monaco Editor, commonly used in VSCode, to build a customizable code editor.</description><pubDate>Mon, 20 Jan 2025 02:32:31 GMT</pubDate><content:encoded>&lt;h1&gt;Lit and Monaco Editor&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to set up a &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; web component and use it to wrap the &lt;a href=&quot;https://microsoft.github.io/monaco-editor/&quot;&gt;Monaco Editor&lt;/a&gt; that powers &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VSCode&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; You can find the final source &lt;a href=&quot;https://github.com/rodydavis/lit-code-editor&quot;&gt;here&lt;/a&gt; and an online demo &lt;a href=&quot;https://rodydavis.github.io/lit-code-editor/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To learn how to build an extension with VSCode and Lit check out the blog post &lt;a href=&quot;https://rodydavis.com/posts/lit-vscode-extension/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Node &amp;gt;= 16&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by navigating in terminal to the location of the project and run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init @vitejs/app --template lit-ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enter a project name &lt;code&gt;lit-code-editor&lt;/code&gt; and now open the project in vscode and install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd lit-code-editor
npm i lit monaco-editor
npm i -D @types/node
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;vite.config.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from &amp;quot;vite&amp;quot;;
import { resolve } from &amp;quot;path&amp;quot;;

export default defineConfig({
  base: &amp;quot;/lit-code-editor/&amp;quot;,
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, &amp;quot;index.html&amp;quot;),
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Template&lt;/h2&gt;
&lt;p&gt;Open up the &lt;code&gt;index.html&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/src/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;Lit Code Editor&amp;lt;/title&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/code-editor.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100vh;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;code-editor&amp;gt;
      &amp;lt;script type=&amp;quot;text/javascript&amp;quot;&amp;gt;
function x() {
  console.log(&amp;quot;Hello world! :)&amp;quot;);
}
      &amp;lt;/script&amp;gt;
    &amp;lt;/code-editor&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are setting up the &lt;code&gt;lit-element&lt;/code&gt; to have a slot which will be the code for the editor to start with. The language can be set with the type or adding an attribute to the &lt;code&gt;code-editor&lt;/code&gt; component.&lt;/p&gt;
&lt;h2&gt;Web Component&lt;/h2&gt;
&lt;p&gt;Before we update our component we need to rename &lt;code&gt;my-element.ts&lt;/code&gt; to &lt;code&gt;code-editor.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open up &lt;code&gt;code-editor.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { css, html, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, property } from &amp;quot;lit/decorators.js&amp;quot;;
import { createRef, Ref, ref } from &amp;quot;lit/directives/ref.js&amp;quot;;

// -- Monaco Editor Imports --
import * as monaco from &amp;quot;monaco-editor&amp;quot;;
import styles from &amp;quot;monaco-editor/min/vs/editor/editor.main.css&amp;quot;;
import editorWorker from &amp;quot;monaco-editor/esm/vs/editor/editor.worker?worker&amp;quot;;
import jsonWorker from &amp;quot;monaco-editor/esm/vs/language/json/json.worker?worker&amp;quot;;
import cssWorker from &amp;quot;monaco-editor/esm/vs/language/css/css.worker?worker&amp;quot;;
import htmlWorker from &amp;quot;monaco-editor/esm/vs/language/html/html.worker?worker&amp;quot;;
import tsWorker from &amp;quot;monaco-editor/esm/vs/language/typescript/ts.worker?worker&amp;quot;;

// @ts-ignore
self.MonacoEnvironment = {
  getWorker(_: any, label: string) {
    if (label === &amp;quot;json&amp;quot;) {
      return new jsonWorker();
    }
    if (label === &amp;quot;css&amp;quot; || label === &amp;quot;scss&amp;quot; || label === &amp;quot;less&amp;quot;) {
      return new cssWorker();
    }
    if (label === &amp;quot;html&amp;quot; || label === &amp;quot;handlebars&amp;quot; || label === &amp;quot;razor&amp;quot;) {
      return new htmlWorker();
    }
    if (label === &amp;quot;typescript&amp;quot; || label === &amp;quot;javascript&amp;quot;) {
      return new tsWorker();
    }
    return new editorWorker();
  },
};

@customElement(&amp;quot;code-editor&amp;quot;)
export class CodeEditor extends LitElement {
  private container: Ref&amp;lt;HTMLElement&amp;gt; = createRef();
  editor?: monaco.editor.IStandaloneCodeEditor;
  @property() theme?: string;
  @property() language?: string;
  @property() code?: string;

  static styles = css`
    :host {
      --editor-width: 100%;
      --editor-height: 100vh;
    }
    main {
      width: var(--editor-width);
      height: var(--editor-height);
    }
  `;

  render() {
    return html`
      &amp;lt;style&amp;gt;
        ${styles}
      &amp;lt;/style&amp;gt;
      &amp;lt;main ${ref(this.container)}&amp;gt;&amp;lt;/main&amp;gt;
    `;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    &amp;quot;code-editor&amp;quot;: CodeEditor;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are just setting up some boilerplate to set up the &lt;a href=&quot;https://vitejs.dev/guide/features.html#web-workers&quot;&gt;web workers with vite&lt;/a&gt; and passing the reference from the container element to the template using the &lt;a href=&quot;https://lit.dev/docs/templates/directives/#ref&quot;&gt;ref directive&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The styles from monaco editor are also passed as a style element load in the shadow root.&lt;/p&gt;
&lt;p&gt;Now let&apos;s add some helper methods for accessing the code and language provided:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;private getFile() {
  if (this.children.length &amp;gt; 0) return this.children[0];
  return null;
}

private getCode() {
  if (this.code) return this.code;
  const file = this.getFile();
  if (!file) return;
  return file.innerHTML.trim();
}

private getLang() {
  if (this.language) return this.language;
  const file = this.getFile();
  if (!file) return;
  const type = file.getAttribute(&amp;quot;type&amp;quot;)!;
  return type.split(&amp;quot;/&amp;quot;).pop()!;
}

private getTheme() {
  if (this.theme) return this.theme;
  if (this.isDark()) return &amp;quot;vs-dark&amp;quot;;
  return &amp;quot;vs-light&amp;quot;;
}

private isDark() {
  return (
    window.matchMedia &amp;amp;&amp;amp;
    window.matchMedia(&amp;quot;(prefers-color-scheme: dark)&amp;quot;).matches
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These methods are checking the slot for the script tag with the language provided or looking for a property set on &lt;code&gt;code-editor&lt;/code&gt; and then returning the value.&lt;/p&gt;
&lt;p&gt;Now let&apos;s attach the editor to the container reference:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;firstUpdated() {
  this.editor = monaco.editor.create(this.container.value!, {
    value: this.getCode(),
    language: this.getLang(),
    theme: this.getTheme(),
    automaticLayout: true,
  });
   window
    .matchMedia(&amp;quot;(prefers-color-scheme: dark)&amp;quot;)
    .addEventListener(&amp;quot;change&amp;quot;, () =&amp;gt; {
      monaco.editor.setTheme(this.getTheme());
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the editor should be running and able to be interacted with:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/m_1_tsfmich8kz.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;When the system changes to dark mode it will &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme&quot;&gt;switch&lt;/a&gt; as well!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/m_2_ggpw2nyy0d.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;To get and set the value from the editor we can add 2 helper methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;setValue(value: string) {
  this.editor!.setValue(value);
}

getValue() {
  const value = this.editor!.getValue();
  return value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything should work as expected now and the final code should look like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { css, html, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, property } from &amp;quot;lit/decorators.js&amp;quot;;
import { createRef, Ref, ref } from &amp;quot;lit/directives/ref.js&amp;quot;;

// -- Monaco Editor Imports --
import * as monaco from &amp;quot;monaco-editor&amp;quot;;
import styles from &amp;quot;monaco-editor/min/vs/editor/editor.main.css&amp;quot;;
import editorWorker from &amp;quot;monaco-editor/esm/vs/editor/editor.worker?worker&amp;quot;;
import jsonWorker from &amp;quot;monaco-editor/esm/vs/language/json/json.worker?worker&amp;quot;;
import cssWorker from &amp;quot;monaco-editor/esm/vs/language/css/css.worker?worker&amp;quot;;
import htmlWorker from &amp;quot;monaco-editor/esm/vs/language/html/html.worker?worker&amp;quot;;
import tsWorker from &amp;quot;monaco-editor/esm/vs/language/typescript/ts.worker?worker&amp;quot;;

// @ts-ignore
self.MonacoEnvironment = {
  getWorker(_: any, label: string) {
    if (label === &amp;quot;json&amp;quot;) {
      return new jsonWorker();
    }
    if (label === &amp;quot;css&amp;quot; || label === &amp;quot;scss&amp;quot; || label === &amp;quot;less&amp;quot;) {
      return new cssWorker();
    }
    if (label === &amp;quot;html&amp;quot; || label === &amp;quot;handlebars&amp;quot; || label === &amp;quot;razor&amp;quot;) {
      return new htmlWorker();
    }
    if (label === &amp;quot;typescript&amp;quot; || label === &amp;quot;javascript&amp;quot;) {
      return new tsWorker();
    }
    return new editorWorker();
  },
};

@customElement(&amp;quot;code-editor&amp;quot;)
export class CodeEditor extends LitElement {
  private container: Ref&amp;lt;HTMLElement&amp;gt; = createRef();
  editor?: monaco.editor.IStandaloneCodeEditor;
  @property() theme?: string;
  @property() language?: string;
  @property() code?: string;

  static styles = css`
    :host {
      --editor-width: 100%;
      --editor-height: 100vh;
    }
    main {
      width: var(--editor-width);
      height: var(--editor-height);
    }
  `;

  render() {
    return html`
      &amp;lt;style&amp;gt;
        ${styles}
      &amp;lt;/style&amp;gt;
      &amp;lt;main ${ref(this.container)}&amp;gt;&amp;lt;/main&amp;gt;
    `;
  }

  private getFile() {
    if (this.children.length &amp;gt; 0) return this.children[0];
    return null;
  }

  private getCode() {
    if (this.code) return this.code;
    const file = this.getFile();
    if (!file) return;
    return file.innerHTML.trim();
  }

  private getLang() {
    if (this.language) return this.language;
    const file = this.getFile();
    if (!file) return;
    const type = file.getAttribute(&amp;quot;type&amp;quot;)!;
    return type.split(&amp;quot;/&amp;quot;).pop()!;
  }

  private getTheme() {
    if (this.theme) return this.theme;
    if (this.isDark()) return &amp;quot;vs-dark&amp;quot;;
    return &amp;quot;vs-light&amp;quot;;
  }

  private isDark() {
    return (
      window.matchMedia &amp;amp;&amp;amp;
      window.matchMedia(&amp;quot;(prefers-color-scheme: dark)&amp;quot;).matches
    );
  }

  setValue(value: string) {
    this.editor!.setValue(value);
  }

  getValue() {
    const value = this.editor!.getValue();
    return value;
  }

  firstUpdated() {
    this.editor = monaco.editor.create(this.container.value!, {
      value: this.getCode(),
      language: this.getLang(),
      theme: this.getTheme(),
      automaticLayout: true,
    });
    window
      .matchMedia(&amp;quot;(prefers-color-scheme: dark)&amp;quot;)
      .addEventListener(&amp;quot;change&amp;quot;, () =&amp;gt; {
        monaco.editor.setTheme(this.getTheme());
      });
  }
}

declare global {
  interface HTMLElementTagNameMap {
    &amp;quot;code-editor&amp;quot;: CodeEditor;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;To use this component it can have the code provided by slots:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;code-editor&amp;gt;
  &amp;lt;script type=&amp;quot;text/javascript&amp;quot;&amp;gt;
function x() {
  console.log(&amp;quot;Hello world! :)&amp;quot;);
}
  &amp;lt;/script&amp;gt;
&amp;lt;/code-editor&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or for properties:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;code-editor 
  code=&amp;quot;console.log(&apos;Hello World&apos;);&amp;quot; 
  language=&amp;quot;javascript&amp;quot;
  &amp;gt;
&amp;lt;/code-editor&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or both:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;code-editor language=&amp;quot;typescript&amp;quot;&amp;gt;
  &amp;lt;script&amp;gt;
function x() {
  console.log(&amp;quot;Hello world! :)&amp;quot;);
}
  &amp;lt;/script&amp;gt;
&amp;lt;/code-editor&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The theme can also be manually set:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;code-editor theme=&amp;quot;vs-light&amp;quot;&amp;gt; &amp;lt;/code-editor&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to learn more about building with Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The source for this example can be found &lt;a href=&quot;https://github.com/rodydavis/lit-code-editor&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Building a Rich Text Editor with Lit</title><link>https://rodydavis.com/lit/rich-text-editor/</link><guid isPermaLink="true">https://rodydavis.com/lit/rich-text-editor/</guid><description>Create a rich text editor using Lit, a web component framework, with setup instructions and a demo available on GitHub and GitHub Pages.</description><pubDate>Sun, 19 Jan 2025 07:04:52 GMT</pubDate><content:encoded>&lt;h1&gt;Building a Rich Text Editor with Lit&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to set up a &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; web component and use it to create a rich text editor.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/lit-html-editor&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/lit-html-editor/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Node &amp;gt;= 16&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by navigating in terminal to the location of the project and run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init @vitejs/app --template lit-ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enter a project name &lt;code&gt;lit-rich-text-editor&lt;/code&gt; and now open the project in vscode and install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd lit-rich-text-editor
npm i @material/mwc-icon-button
npm i -D @types/node
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;vite.config.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from &amp;quot;vite&amp;quot;;
import { resolve } from &amp;quot;path&amp;quot;;

export default defineConfig({
  base: &apos;/lit-rich-text-editor/&apos;,
  build: {
    lib: {
      entry: &amp;quot;src/lit-rich-text-editor.ts&amp;quot;,
      formats: [&amp;quot;es&amp;quot;],
    },
    rollupOptions: {
      input: {
        main: resolve(__dirname, &amp;quot;index.html&amp;quot;),
      },
    },
  },
});

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Template&lt;/h2&gt;
&lt;p&gt;Open up the &lt;code&gt;index.html&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/src/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;link
      href=&amp;quot;https://fonts.googleapis.com/css?family=Material+Icons&amp;amp;display=block&amp;quot;
      rel=&amp;quot;stylesheet&amp;quot;
    /&amp;gt;
    &amp;lt;title&amp;gt;Lit Rich Text Editor&amp;lt;/title&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/lit-rich-text-editor.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        padding: 0;
        margin: 0;
      }
      lit-rich-text-editor {
        --editor-width: 100%;
        --editor-height: 100vh;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;lit-rich-text-editor&amp;gt;
     &amp;lt;template&amp;gt;
        &amp;lt;h1&amp;gt;Headline 1&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;This is a paragraph.&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;
          &amp;lt;span style=&amp;quot;background-color: rgb(255, 0, 0)&amp;quot;
            &amp;gt;&amp;lt;font color=&amp;quot;#ffffff&amp;quot;&amp;gt;Styled Text&amp;lt;/font&amp;gt;&amp;lt;/span
          &amp;gt;
        &amp;lt;/p&amp;gt;

        &amp;lt;p&amp;gt;
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
          minim veniam, quis nostrud exercitation ullamco laboris nisi ut
          aliquip ex ea commodo consequat. Duis aute irure dolor in
          reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
          pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
          culpa qui officia deserunt mollit anim id est laborum.
        &amp;lt;/p&amp;gt;
      &amp;lt;/template&amp;gt;
    &amp;lt;/lit-rich-text-editor&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important things to take away are the styles added to remove the body padding and send size &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/--*&quot;&gt;CSS Custom Properties&lt;/a&gt; to the editor to take up the full viewport.&lt;/p&gt;
&lt;p&gt;Inside the &lt;code&gt;lit-rich-text-editor&lt;/code&gt; tags there is a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template&quot;&gt;&lt;code&gt;template&lt;/code&gt;&lt;/a&gt; passed as a slot to provide html that will not be rendered but can be accessed.&lt;/p&gt;
&lt;p&gt;There is also an import for the &lt;a href=&quot;https://fonts.google.com/icons&quot;&gt;Material Icons&lt;/a&gt; so it can be used in the editor later.&lt;/p&gt;
&lt;h2&gt;Editor&lt;/h2&gt;
&lt;p&gt;The next thing to create is the editor itself. Open up &lt;code&gt;src/lit-rich-text-editor.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, css, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, property, state } from &amp;quot;lit/decorators.js&amp;quot;;

import &amp;quot;@material/mwc-icon-button&amp;quot;;

@customElement(&amp;quot;lit-rich-text-editor&amp;quot;)
export class LitRichTextEditor extends LitElement {
  @state() content: string = &amp;quot;&amp;quot;;
  @state() root: Element | null = null;

  static styles = css`
    :host {
      --editor-width: 600px;
      --editor-height: 600px;
      --editor-background: #f1f1f1;
      --editor-toolbar-height: 33px;
      --editor-toolbar-background: black;
      --editor-toolbar-on-background: white;
      --editor-toolbar-on-active-background: #a4a4a4;
    }
    main {
      width: var(--editor-width);
      height: var(--editor-height);
      display: grid;
      grid-template-areas:
        &amp;quot;toolbar toolbar&amp;quot;
        &amp;quot;editor editor&amp;quot;;
      grid-template-rows: var(--editor-toolbar-height) auto;
      grid-template-columns: auto auto;
    }
    #editor-actions {
      grid-area: toolbar;
      width: var(--editor-width);
      height: var(--editor-toolbar-height);
      background-color: var(--editor-toolbar-background);
      color: var(--editor-toolbar-on-background);
      overscroll-behavior: contain;
      overflow-y: auto;
      -ms-overflow-style: none;
      scrollbar-width: none;
    }
    #editor-actions::-webkit-scrollbar {
      display: none;
    }
    #editor {
      width: var(--editor-width);
      grid-area: editor;
      background-color: var(--editor-background);
    }
    #toolbar {
      width: 1090px;
      height: var(--editor-toolbar-height);
    }
    [contenteditable] {
      outline: 0px solid transparent;
    }
    #toolbar &amp;gt; mwc-icon-button {
      color: var(--editor-toolbar-on-background);
      --mdc-icon-size: 20px;
      --mdc-icon-button-size: 30px;
      cursor: pointer;
    }
    #toolbar &amp;gt; .active {
      color: var(--editor-toolbar-on-active-background);
    }
    select {
      margin-top: 5px;
      height: calc(var(--editor-toolbar-height) - 10px);
    }
    input[type=&amp;quot;color&amp;quot;] {
      height: calc(var(--editor-toolbar-height) - 15px);
      -webkit-appearance: none;
      border: none;
      width: 22px;
    }
    input[type=&amp;quot;color&amp;quot;]::-webkit-color-swatch-wrapper {
      padding: 0;
    }
    input[type=&amp;quot;color&amp;quot;]::-webkit-color-swatch {
      border: none;
    }
  `;

  render() {
    return html`&amp;lt;main&amp;gt;
      &amp;lt;input id=&amp;quot;bg&amp;quot; type=&amp;quot;color&amp;quot; style=&amp;quot;display:none&amp;quot; /&amp;gt;
      &amp;lt;input id=&amp;quot;fg&amp;quot; type=&amp;quot;color&amp;quot; style=&amp;quot;display:none&amp;quot; /&amp;gt;
      &amp;lt;div id=&amp;quot;editor-actions&amp;quot;&amp;gt;
        &amp;lt;div id=&amp;quot;toolbar&amp;quot;&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div id=&amp;quot;editor&amp;quot;&amp;gt;${this.root}&amp;lt;/div&amp;gt;
    &amp;lt;/main&amp;gt; `;
  }

  async firstUpdated() {
    const elem = this.parentElement!.querySelector(&amp;quot;lit-rich-text-editor template&amp;quot;);
    this.content = elem?.innerHTML ?? &amp;quot;&amp;quot;;
    this.reset();
  }

  reset() {
    const parser = new DOMParser();
    const doc = parser.parseFromString(this.content, &amp;quot;text/html&amp;quot;);
    document.execCommand(&amp;quot;defaultParagraphSeparator&amp;quot;, false, &amp;quot;br&amp;quot;);
    document.addEventListener(&amp;quot;selectionchange&amp;quot;, () =&amp;gt; {
      this.requestUpdate();
    });
    const root = doc.querySelector(&amp;quot;body&amp;quot;);
    root!.setAttribute(&amp;quot;contenteditable&amp;quot;, &amp;quot;true&amp;quot;);
    this.root = root;
  }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With everything updated run &lt;code&gt;npm run dev&lt;/code&gt; and the following should appear in the browser:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/lit_piano_1_32gx6viodw.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Nothing special is happening yet, but the template is being read and passed into the element, parsed and setting the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable&quot;&gt;&lt;code&gt;contenteditable&lt;/code&gt;&lt;/a&gt; attribute to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is a way to access the slots and use the nodes to hold data that are not used for rendering. Doing it this way allows for a transformation of the HTML source into a format that can be used.&lt;/p&gt;
&lt;h2&gt;Toolbar&lt;/h2&gt;
&lt;p&gt;At the bottom of the class before the last &lt;code&gt;}&lt;/code&gt; add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;renderToolbar(command: (c: string, val: string | undefined) =&amp;gt; void) {
    // TODO: Selection does not work on Safari iOS
    const selection = this.shadowRoot?.getSelection
      ? this.shadowRoot!.getSelection()
      : null;
    const tags: string[] = [];
    if (selection?.type === &amp;quot;Range&amp;quot;) {
      // @ts-ignore
      let parentNode = selection?.baseNode;
      if (parentNode) {
        const checkNode = () =&amp;gt; {
          const parentTagName = parentNode?.tagName?.toLowerCase()?.trim();
          if (parentTagName) tags.push(parentTagName);
        };
        while (parentNode != null) {
          checkNode();
          parentNode = parentNode?.parentNode;
        }
      }
    }

    const commands: {
      icon: string;
      command: string | (() =&amp;gt; void);
      active?: boolean;
      type?: string;
      values?: { value: string; name: string; font?: boolean }[];
      command_value?: string;
    }[] = [
      {
        icon: &amp;quot;format_clear&amp;quot;,
        command: &amp;quot;removeFormat&amp;quot;,
      },

      {
        icon: &amp;quot;format_bold&amp;quot;,
        command: &amp;quot;bold&amp;quot;,
        active: tags.includes(&amp;quot;b&amp;quot;),
      },
      {
        icon: &amp;quot;format_italic&amp;quot;,
        command: &amp;quot;italic&amp;quot;,
        active: tags.includes(&amp;quot;i&amp;quot;),
      },
      {
        icon: &amp;quot;format_underlined&amp;quot;,
        command: &amp;quot;underline&amp;quot;,
        active: tags.includes(&amp;quot;u&amp;quot;),
      },
      {
        icon: &amp;quot;format_align_left&amp;quot;,
        command: &amp;quot;justifyleft&amp;quot;,
      },
      {
        icon: &amp;quot;format_align_center&amp;quot;,
        command: &amp;quot;justifycenter&amp;quot;,
      },
      {
        icon: &amp;quot;format_align_right&amp;quot;,
        command: &amp;quot;justifyright&amp;quot;,
      },
      {
        icon: &amp;quot;format_list_numbered&amp;quot;,
        command: &amp;quot;insertorderedlist&amp;quot;,
        active: tags.includes(&amp;quot;ol&amp;quot;),
      },
      {
        icon: &amp;quot;format_list_bulleted&amp;quot;,
        command: &amp;quot;insertunorderedlist&amp;quot;,
        active: tags.includes(&amp;quot;ul&amp;quot;),
      },
      {
        icon: &amp;quot;format_quote&amp;quot;,
        command: &amp;quot;formatblock&amp;quot;,
        command_value: &amp;quot;blockquote&amp;quot;,
      },
      {
        icon: &amp;quot;format_indent_decrease&amp;quot;,
        command: &amp;quot;outdent&amp;quot;,
      },
      {
        icon: &amp;quot;format_indent_increase&amp;quot;,
        command: &amp;quot;indent&amp;quot;,
      },

      {
        icon: &amp;quot;add_link&amp;quot;,
        command: () =&amp;gt; {
          const newLink = prompt(&amp;quot;Write the URL here&amp;quot;, &amp;quot;http://&amp;quot;);
          if (newLink &amp;amp;&amp;amp; newLink != &amp;quot;&amp;quot; &amp;amp;&amp;amp; newLink != &amp;quot;http://&amp;quot;) {
            command(&amp;quot;createlink&amp;quot;, newLink);
          }
        },
      },
      { icon: &amp;quot;link_off&amp;quot;, command: &amp;quot;unlink&amp;quot; },
      {
        icon: &amp;quot;format_color_text&amp;quot;,
        command: () =&amp;gt; {
          const input = this.shadowRoot!.querySelector(
            &amp;quot;#fg&amp;quot;
          )! as HTMLInputElement;
          input.addEventListener(&amp;quot;input&amp;quot;, (e: any) =&amp;gt; {
            const val = e.target.value;
            command(&amp;quot;forecolor&amp;quot;, val);
          });
          input.click();
        },
        type: &amp;quot;color&amp;quot;,
      },
      {
        icon: &amp;quot;border_color&amp;quot;,
        command: () =&amp;gt; {
          const input = this.shadowRoot!.querySelector(
            &amp;quot;#bg&amp;quot;
          )! as HTMLInputElement;
          input.addEventListener(&amp;quot;input&amp;quot;, (e: any) =&amp;gt; {
            const val = e.target.value;
            command(&amp;quot;backcolor&amp;quot;, val);
          });
          input.click();
        },
        type: &amp;quot;color&amp;quot;,
      },
      {
        icon: &amp;quot;title&amp;quot;,
        command: &amp;quot;formatblock&amp;quot;,
        values: [
          { name: &amp;quot;Normal Text&amp;quot;, value: &amp;quot;--&amp;quot; },
          { name: &amp;quot;Heading 1&amp;quot;, value: &amp;quot;h1&amp;quot; },
          { name: &amp;quot;Heading 2&amp;quot;, value: &amp;quot;h2&amp;quot; },
          { name: &amp;quot;Heading 3&amp;quot;, value: &amp;quot;h3&amp;quot; },
          { name: &amp;quot;Heading 4&amp;quot;, value: &amp;quot;h4&amp;quot; },
          { name: &amp;quot;Heading 5&amp;quot;, value: &amp;quot;h5&amp;quot; },
          { name: &amp;quot;Heading 6&amp;quot;, value: &amp;quot;h6&amp;quot; },
          { name: &amp;quot;Paragraph&amp;quot;, value: &amp;quot;p&amp;quot; },
          { name: &amp;quot;Pre-Formatted&amp;quot;, value: &amp;quot;pre&amp;quot; },
        ],
      },
      {
        icon: &amp;quot;text_format&amp;quot;,
        command: &amp;quot;fontname&amp;quot;,
        values: [
          { name: &amp;quot;Font Name&amp;quot;, value: &amp;quot;--&amp;quot; },
          ...[...checkFonts()].map((f) =&amp;gt; ({
            name: f,
            value: f,
            font: true,
          })),
        ],
      },
      {
        icon: &amp;quot;format_size&amp;quot;,
        command: &amp;quot;fontsize&amp;quot;,
        values: [
          { name: &amp;quot;Font Size&amp;quot;, value: &amp;quot;--&amp;quot; },
          { name: &amp;quot;Very Small&amp;quot;, value: &amp;quot;1&amp;quot; },
          { name: &amp;quot;Small&amp;quot;, value: &amp;quot;2&amp;quot; },
          { name: &amp;quot;Normal&amp;quot;, value: &amp;quot;3&amp;quot; },
          { name: &amp;quot;Medium Large&amp;quot;, value: &amp;quot;4&amp;quot; },
          { name: &amp;quot;Large&amp;quot;, value: &amp;quot;5&amp;quot; },
          { name: &amp;quot;Very Large&amp;quot;, value: &amp;quot;6&amp;quot; },
          { name: &amp;quot;Maximum&amp;quot;, value: &amp;quot;7&amp;quot; },
        ],
      },
      {
        icon: &amp;quot;undo&amp;quot;,
        command: &amp;quot;undo&amp;quot;,
      },
      {
        icon: &amp;quot;redo&amp;quot;,
        command: &amp;quot;redo&amp;quot;,
      },
      {
        icon: &amp;quot;content_cut&amp;quot;,
        command: &amp;quot;cut&amp;quot;,
      },
      {
        icon: &amp;quot;content_copy&amp;quot;,
        command: &amp;quot;copy&amp;quot;,
      },
      {
        icon: &amp;quot;content_paste&amp;quot;,
        command: &amp;quot;paste&amp;quot;,
      },
    ];

    return html`
      ${commands.map((n) =&amp;gt; {
        return html`
          ${n.values
            ? html` &amp;lt;select
                id=&amp;quot;${n.icon}&amp;quot;
                @change=${(e: any) =&amp;gt; {
                  const val = e.target.value;
                  if (val === &amp;quot;--&amp;quot;) {
                    command(&amp;quot;removeFormat&amp;quot;, undefined);
                  } else if (typeof n.command === &amp;quot;string&amp;quot;) {
                    command(n.command, val);
                  }
                }}
              &amp;gt;
                ${n.values.map(
                  (v) =&amp;gt; html` &amp;lt;option value=${v.value}&amp;gt;${v.name}&amp;lt;/option&amp;gt;`
                )}
              &amp;lt;/select&amp;gt;`
            : html` &amp;lt;mwc-icon-button
                icon=&amp;quot;${n.icon}&amp;quot;
                class=&amp;quot;${n.active ? &amp;quot;active&amp;quot; : &amp;quot;inactive&amp;quot;}&amp;quot;
                @click=${() =&amp;gt; {
                  if (n.values) {
                  } else if (typeof n.command === &amp;quot;string&amp;quot;) {
                    command(n.command, n.command_value);
                  } else {
                    n.command();
                  }
                }}
              &amp;gt;&amp;lt;/mwc-icon-button&amp;gt;`}
        `;
      })}
    `;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This takes an array of objects that we can map to &lt;code&gt;mwc-icon-button&lt;/code&gt; or &lt;code&gt;select&lt;/code&gt; depending on the passed values. This will also set up the event listeners and execute the command for the given action.&lt;/p&gt;
&lt;p&gt;Inside the &lt;code&gt;&amp;lt;div id=&amp;quot;toolbar&amp;quot;&amp;gt;&lt;/code&gt; tag add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;${this.renderToolbar((command, val) =&amp;gt; {
    document.execCommand(command, false, val);
    console.log(&amp;quot;command&amp;quot;, command, val);
})}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will listen for the callback and fire the command on the document and log it to the console.&lt;/p&gt;
&lt;p&gt;And finally at the bottom of the file add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export function checkFonts(): string[] {
  const fontCheck = new Set(
    [
      // Windows 10
      &amp;quot;Arial&amp;quot;,
      &amp;quot;Arial Black&amp;quot;,
      &amp;quot;Bahnschrift&amp;quot;,
      &amp;quot;Calibri&amp;quot;,
      &amp;quot;Cambria&amp;quot;,
      &amp;quot;Cambria Math&amp;quot;,
      &amp;quot;Candara&amp;quot;,
      &amp;quot;Comic Sans MS&amp;quot;,
      &amp;quot;Consolas&amp;quot;,
      &amp;quot;Constantia&amp;quot;,
      &amp;quot;Corbel&amp;quot;,
      &amp;quot;Courier New&amp;quot;,
      &amp;quot;Ebrima&amp;quot;,
      &amp;quot;Franklin Gothic Medium&amp;quot;,
      &amp;quot;Gabriola&amp;quot;,
      &amp;quot;Gadugi&amp;quot;,
      &amp;quot;Georgia&amp;quot;,
      &amp;quot;HoloLens MDL2 Assets&amp;quot;,
      &amp;quot;Impact&amp;quot;,
      &amp;quot;Ink Free&amp;quot;,
      &amp;quot;Javanese Text&amp;quot;,
      &amp;quot;Leelawadee UI&amp;quot;,
      &amp;quot;Lucida Console&amp;quot;,
      &amp;quot;Lucida Sans Unicode&amp;quot;,
      &amp;quot;Malgun Gothic&amp;quot;,
      &amp;quot;Marlett&amp;quot;,
      &amp;quot;Microsoft Himalaya&amp;quot;,
      &amp;quot;Microsoft JhengHei&amp;quot;,
      &amp;quot;Microsoft New Tai Lue&amp;quot;,
      &amp;quot;Microsoft PhagsPa&amp;quot;,
      &amp;quot;Microsoft Sans Serif&amp;quot;,
      &amp;quot;Microsoft Tai Le&amp;quot;,
      &amp;quot;Microsoft YaHei&amp;quot;,
      &amp;quot;Microsoft Yi Baiti&amp;quot;,
      &amp;quot;MingLiU-ExtB&amp;quot;,
      &amp;quot;Mongolian Baiti&amp;quot;,
      &amp;quot;MS Gothic&amp;quot;,
      &amp;quot;MV Boli&amp;quot;,
      &amp;quot;Myanmar Text&amp;quot;,
      &amp;quot;Nirmala UI&amp;quot;,
      &amp;quot;Palatino Linotype&amp;quot;,
      &amp;quot;Segoe MDL2 Assets&amp;quot;,
      &amp;quot;Segoe Print&amp;quot;,
      &amp;quot;Segoe Script&amp;quot;,
      &amp;quot;Segoe UI&amp;quot;,
      &amp;quot;Segoe UI Historic&amp;quot;,
      &amp;quot;Segoe UI Emoji&amp;quot;,
      &amp;quot;Segoe UI Symbol&amp;quot;,
      &amp;quot;SimSun&amp;quot;,
      &amp;quot;Sitka&amp;quot;,
      &amp;quot;Sylfaen&amp;quot;,
      &amp;quot;Symbol&amp;quot;,
      &amp;quot;Tahoma&amp;quot;,
      &amp;quot;Times New Roman&amp;quot;,
      &amp;quot;Trebuchet MS&amp;quot;,
      &amp;quot;Verdana&amp;quot;,
      &amp;quot;Webdings&amp;quot;,
      &amp;quot;Wingdings&amp;quot;,
      &amp;quot;Yu Gothic&amp;quot;,
      // macOS
      &amp;quot;American Typewriter&amp;quot;,
      &amp;quot;Andale Mono&amp;quot;,
      &amp;quot;Arial&amp;quot;,
      &amp;quot;Arial Black&amp;quot;,
      &amp;quot;Arial Narrow&amp;quot;,
      &amp;quot;Arial Rounded MT Bold&amp;quot;,
      &amp;quot;Arial Unicode MS&amp;quot;,
      &amp;quot;Avenir&amp;quot;,
      &amp;quot;Avenir Next&amp;quot;,
      &amp;quot;Avenir Next Condensed&amp;quot;,
      &amp;quot;Baskerville&amp;quot;,
      &amp;quot;Big Caslon&amp;quot;,
      &amp;quot;Bodoni 72&amp;quot;,
      &amp;quot;Bodoni 72 Oldstyle&amp;quot;,
      &amp;quot;Bodoni 72 Smallcaps&amp;quot;,
      &amp;quot;Bradley Hand&amp;quot;,
      &amp;quot;Brush Script MT&amp;quot;,
      &amp;quot;Chalkboard&amp;quot;,
      &amp;quot;Chalkboard SE&amp;quot;,
      &amp;quot;Chalkduster&amp;quot;,
      &amp;quot;Charter&amp;quot;,
      &amp;quot;Cochin&amp;quot;,
      &amp;quot;Comic Sans MS&amp;quot;,
      &amp;quot;Copperplate&amp;quot;,
      &amp;quot;Courier&amp;quot;,
      &amp;quot;Courier New&amp;quot;,
      &amp;quot;Didot&amp;quot;,
      &amp;quot;DIN Alternate&amp;quot;,
      &amp;quot;DIN Condensed&amp;quot;,
      &amp;quot;Futura&amp;quot;,
      &amp;quot;Geneva&amp;quot;,
      &amp;quot;Georgia&amp;quot;,
      &amp;quot;Gill Sans&amp;quot;,
      &amp;quot;Helvetica&amp;quot;,
      &amp;quot;Helvetica Neue&amp;quot;,
      &amp;quot;Herculanum&amp;quot;,
      &amp;quot;Hoefler Text&amp;quot;,
      &amp;quot;Impact&amp;quot;,
      &amp;quot;Lucida Grande&amp;quot;,
      &amp;quot;Luminari&amp;quot;,
      &amp;quot;Marker Felt&amp;quot;,
      &amp;quot;Menlo&amp;quot;,
      &amp;quot;Microsoft Sans Serif&amp;quot;,
      &amp;quot;Monaco&amp;quot;,
      &amp;quot;Noteworthy&amp;quot;,
      &amp;quot;Optima&amp;quot;,
      &amp;quot;Palatino&amp;quot;,
      &amp;quot;Papyrus&amp;quot;,
      &amp;quot;Phosphate&amp;quot;,
      &amp;quot;Rockwell&amp;quot;,
      &amp;quot;Savoye LET&amp;quot;,
      &amp;quot;SignPainter&amp;quot;,
      &amp;quot;Skia&amp;quot;,
      &amp;quot;Snell Roundhand&amp;quot;,
      &amp;quot;Tahoma&amp;quot;,
      &amp;quot;Times&amp;quot;,
      &amp;quot;Times New Roman&amp;quot;,
      &amp;quot;Trattatello&amp;quot;,
      &amp;quot;Trebuchet MS&amp;quot;,
      &amp;quot;Verdana&amp;quot;,
      &amp;quot;Zapfino&amp;quot;,
    ].sort()
  );
  const fontAvailable = new Set&amp;lt;string&amp;gt;();
  // @ts-ignore
  for (const font of fontCheck.values()) {
    // @ts-ignore
    if (document.fonts.check(`12px &amp;quot;${font}&amp;quot;`)) {
      fontAvailable.add(font);
    }
  }
  // @ts-ignore
  return fontAvailable.values();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Following this great suggestion &lt;a href=&quot;https://stackoverflow.com/a/62755574/7303311&quot;&gt;here&lt;/a&gt; the document checks to see all the avaliable fonts for the browser and given document.&lt;/p&gt;
&lt;h2&gt;Running&lt;/h2&gt;
&lt;p&gt;If everything went well when the command &lt;code&gt;npm run dev&lt;/code&gt; is run the following should appear in the viewport:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/lit_text_2_y8vvurvfh1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to learn more about building with Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The source for this example can be found &lt;a href=&quot;https://github.com/rodydavis/lit-html-editor&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Lit Sheet Music</title><link>https://rodydavis.com/lit/sheet-music/</link><guid isPermaLink="true">https://rodydavis.com/lit/sheet-music/</guid><description>Render sheet music from MusicXML in a web component using Lit and OpenSheetMusicDisplay, with responsive resizing for different screen widths.</description><pubDate>Mon, 20 Jan 2025 02:46:46 GMT</pubDate><content:encoded>&lt;h1&gt;Lit Sheet Music&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to set up a &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; web component and use it to render &lt;a href=&quot;https://www.musicxml.com/&quot;&gt;musicxml&lt;/a&gt; from a src attribute or inline xml using &lt;a href=&quot;https://github.com/opensheetmusicdisplay/opensheetmusicdisplay&quot;&gt;opensheetmusicdisplay&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/nice_rbhmdu7o6t.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now any sheet music can be rendered based on the browser width as an svg or canvas (and will resize when the viewport changes).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/lit-sheet-music&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/lit-sheet-music/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Node &amp;gt;= 16&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by navigating in terminal to the location of the project and run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init @vitejs/app --template lit-ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enter a project name &lt;code&gt;lit-sheet-music&lt;/code&gt; and now open the project in vscode and install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd lit-sheet-music
npm i lit opensheetmusicdisplay
npm i -D @types/node
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;vite.config.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from &amp;quot;vite&amp;quot;;
import { resolve } from &amp;quot;path&amp;quot;;

export default defineConfig({
  base: &amp;quot;/lit-sheet-music/&amp;quot;,
  build: {
    lib: {
      entry: &amp;quot;src/lit-sheet-music.ts&amp;quot;,
      formats: [&amp;quot;es&amp;quot;],
    },
    rollupOptions: {
      input: {
        main: resolve(__dirname, &amp;quot;index.html&amp;quot;),
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Template&lt;/h2&gt;
&lt;p&gt;Open up the &lt;code&gt;index.html&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/src/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;Lit Sheet Music&amp;lt;/title&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/sheet-music.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        margin: 0;
        padding: 0;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;sheet-music
      src=&amp;quot;https://raw.githubusercontent.com/opensheetmusicdisplay/opensheetmusicdisplay/develop/demo/BrahWiMeSample.musicxml&amp;quot;
    &amp;gt;
    &amp;lt;/sheet-music&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If local &lt;a href=&quot;https://www.musicxml.com/&quot;&gt;musicxml&lt;/a&gt; is intended to be used update &lt;code&gt;index.html&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/src/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;Lit Sheet Music&amp;lt;/title&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/sheet-music.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        margin: 0;
        padding: 0;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;sheet-music&amp;gt;
      &amp;lt;script type=&amp;quot;text/xml&amp;quot;&amp;gt;
        &amp;lt;?xml version=&amp;quot;1.0&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;
        &amp;lt;!DOCTYPE score-partwise PUBLIC
            &amp;quot;-//Recordare//DTD MusicXML Partwise//EN&amp;quot;
            &amp;quot;http://www.musicxml.org/dtds/partwise.dtd&amp;quot;&amp;gt;
          &amp;lt;score-partwise&amp;gt;
            &amp;lt;part-list&amp;gt;
              &amp;lt;score-part id=&amp;quot;P1&amp;quot;&amp;gt;
                &amp;lt;part-name&amp;gt;Voice&amp;lt;/part-name&amp;gt;
              &amp;lt;/score-part&amp;gt;
            &amp;lt;/part-list&amp;gt;
            &amp;lt;part id=&amp;quot;P1&amp;quot;&amp;gt;
              &amp;lt;measure number=&amp;quot;0&amp;quot; implicit=&amp;quot;yes&amp;quot;&amp;gt;
                &amp;lt;attributes&amp;gt;
                  &amp;lt;divisions&amp;gt;4&amp;lt;/divisions&amp;gt;
                  &amp;lt;key&amp;gt;
                    &amp;lt;fifths&amp;gt;-3&amp;lt;/fifths&amp;gt;
                    &amp;lt;mode&amp;gt;major&amp;lt;/mode&amp;gt;
                  &amp;lt;/key&amp;gt;
                  &amp;lt;time&amp;gt;
                    &amp;lt;beats&amp;gt;2&amp;lt;/beats&amp;gt;
                    &amp;lt;beat-type&amp;gt;4&amp;lt;/beat-type&amp;gt;
                  &amp;lt;/time&amp;gt;
                  &amp;lt;clef&amp;gt;
                    &amp;lt;sign&amp;gt;G&amp;lt;/sign&amp;gt;
                    &amp;lt;line&amp;gt;2&amp;lt;/line&amp;gt;
                  &amp;lt;/clef&amp;gt;
                  &amp;lt;directive&amp;gt;Langsam, innig.&amp;lt;/directive&amp;gt;
                &amp;lt;/attributes&amp;gt;
                &amp;lt;note&amp;gt;
                  &amp;lt;pitch&amp;gt;
                    &amp;lt;step&amp;gt;G&amp;lt;/step&amp;gt;
                    &amp;lt;octave&amp;gt;4&amp;lt;/octave&amp;gt;
                  &amp;lt;/pitch&amp;gt;
                  &amp;lt;duration&amp;gt;2&amp;lt;/duration&amp;gt;
                  &amp;lt;type&amp;gt;eighth&amp;lt;/type&amp;gt;
                  &amp;lt;stem&amp;gt;up&amp;lt;/stem&amp;gt;
                  &amp;lt;notations&amp;gt;
                    &amp;lt;dynamics&amp;gt;
                      &amp;lt;p/&amp;gt;
                    &amp;lt;/dynamics&amp;gt;
                  &amp;lt;/notations&amp;gt;
                  &amp;lt;lyric&amp;gt;
                    &amp;lt;syllabic&amp;gt;single&amp;lt;/syllabic&amp;gt;
                    &amp;lt;text&amp;gt;W&amp;amp;auml;rst&amp;lt;/text&amp;gt;
                  &amp;lt;/lyric&amp;gt;
                &amp;lt;/note&amp;gt;
              &amp;lt;/measure&amp;gt;
              &amp;lt;measure number=&amp;quot;1&amp;quot;&amp;gt;
                &amp;lt;note&amp;gt;
                  &amp;lt;pitch&amp;gt;
                    &amp;lt;step&amp;gt;F&amp;lt;/step&amp;gt;
                    &amp;lt;octave&amp;gt;4&amp;lt;/octave&amp;gt;
                  &amp;lt;/pitch&amp;gt;
                  &amp;lt;duration&amp;gt;3&amp;lt;/duration&amp;gt;
                  &amp;lt;type&amp;gt;eighth&amp;lt;/type&amp;gt;
                  &amp;lt;dot/&amp;gt;
                  &amp;lt;stem&amp;gt;up&amp;lt;/stem&amp;gt;
                  &amp;lt;lyric&amp;gt;
                    &amp;lt;syllabic&amp;gt;single&amp;lt;/syllabic&amp;gt;
                    &amp;lt;text&amp;gt;du&amp;lt;/text&amp;gt;
                  &amp;lt;/lyric&amp;gt;
                &amp;lt;/note&amp;gt;
                &amp;lt;note&amp;gt;
                  &amp;lt;pitch&amp;gt;
                    &amp;lt;step&amp;gt;E&amp;lt;/step&amp;gt;
                    &amp;lt;alter&amp;gt;-1&amp;lt;/alter&amp;gt;
                    &amp;lt;octave&amp;gt;4&amp;lt;/octave&amp;gt;
                  &amp;lt;/pitch&amp;gt;
                  &amp;lt;duration&amp;gt;1&amp;lt;/duration&amp;gt;
                  &amp;lt;type&amp;gt;16th&amp;lt;/type&amp;gt;
                  &amp;lt;stem&amp;gt;up&amp;lt;/stem&amp;gt;
                  &amp;lt;lyric&amp;gt;
                    &amp;lt;syllabic&amp;gt;single&amp;lt;/syllabic&amp;gt;
                    &amp;lt;text&amp;gt;nicht,&amp;lt;/text&amp;gt;
                  &amp;lt;/lyric&amp;gt;
                &amp;lt;/note&amp;gt;
                &amp;lt;note&amp;gt;
                  &amp;lt;pitch&amp;gt;
                    &amp;lt;step&amp;gt;E&amp;lt;/step&amp;gt;
                    &amp;lt;alter&amp;gt;-1&amp;lt;/alter&amp;gt;
                    &amp;lt;octave&amp;gt;4&amp;lt;/octave&amp;gt;
                  &amp;lt;/pitch&amp;gt;
                  &amp;lt;duration&amp;gt;2&amp;lt;/duration&amp;gt;
                  &amp;lt;type&amp;gt;eighth&amp;lt;/type&amp;gt;
                  &amp;lt;stem&amp;gt;up&amp;lt;/stem&amp;gt;
                  &amp;lt;lyric&amp;gt;
                    &amp;lt;syllabic&amp;gt;begin&amp;lt;/syllabic&amp;gt;
                    &amp;lt;text&amp;gt;heil&amp;lt;/text&amp;gt;
                  &amp;lt;/lyric&amp;gt;
                &amp;lt;/note&amp;gt;
                &amp;lt;note&amp;gt;
                  &amp;lt;pitch&amp;gt;
                    &amp;lt;step&amp;gt;B&amp;lt;/step&amp;gt;
                    &amp;lt;alter&amp;gt;-1&amp;lt;/alter&amp;gt;
                    &amp;lt;octave&amp;gt;4&amp;lt;/octave&amp;gt;
                  &amp;lt;/pitch&amp;gt;
                  &amp;lt;duration&amp;gt;1&amp;lt;/duration&amp;gt;
                  &amp;lt;type&amp;gt;16th&amp;lt;/type&amp;gt;
                  &amp;lt;stem&amp;gt;up&amp;lt;/stem&amp;gt;
                  &amp;lt;beam number=&amp;quot;1&amp;quot;&amp;gt;begin&amp;lt;/beam&amp;gt;
                  &amp;lt;beam number=&amp;quot;2&amp;quot;&amp;gt;begin&amp;lt;/beam&amp;gt;
                  &amp;lt;notations&amp;gt;
                    &amp;lt;slur type=&amp;quot;start&amp;quot; number=&amp;quot;1&amp;quot;/&amp;gt;
                  &amp;lt;/notations&amp;gt;
                  &amp;lt;lyric&amp;gt;
                    &amp;lt;syllabic&amp;gt;end&amp;lt;/syllabic&amp;gt;
                    &amp;lt;text&amp;gt;ger&amp;lt;/text&amp;gt;
                    &amp;lt;extend/&amp;gt;
                  &amp;lt;/lyric&amp;gt;
                &amp;lt;/note&amp;gt;
                &amp;lt;note&amp;gt;
                  &amp;lt;pitch&amp;gt;
                    &amp;lt;step&amp;gt;G&amp;lt;/step&amp;gt;
                    &amp;lt;octave&amp;gt;4&amp;lt;/octave&amp;gt;
                  &amp;lt;/pitch&amp;gt;
                  &amp;lt;duration&amp;gt;1&amp;lt;/duration&amp;gt;
                  &amp;lt;type&amp;gt;16th&amp;lt;/type&amp;gt;
                  &amp;lt;stem&amp;gt;up&amp;lt;/stem&amp;gt;
                  &amp;lt;beam number=&amp;quot;1&amp;quot;&amp;gt;end&amp;lt;/beam&amp;gt;
                  &amp;lt;beam number=&amp;quot;2&amp;quot;&amp;gt;end&amp;lt;/beam&amp;gt;
                  &amp;lt;notations&amp;gt;
                    &amp;lt;slur type=&amp;quot;stop&amp;quot; number=&amp;quot;1&amp;quot;/&amp;gt;
                  &amp;lt;/notations&amp;gt;
                  &amp;lt;lyric&amp;gt;
                    &amp;lt;extend/&amp;gt;
                  &amp;lt;/lyric&amp;gt;
                &amp;lt;/note&amp;gt;
              &amp;lt;/measure&amp;gt;
            &amp;lt;/part&amp;gt;
          &amp;lt;/score-partwise&amp;gt;
      &amp;lt;/script&amp;gt;
    &amp;lt;/sheet-music&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are passing a src attribute to the web component for this example but we can also add a script tag with the type attribute set to &lt;code&gt;text/xml&lt;/code&gt; with the contents containing the json.&lt;/p&gt;
&lt;h2&gt;Web Component&lt;/h2&gt;
&lt;p&gt;Before we update our component we need to rename &lt;code&gt;my-element.ts&lt;/code&gt; to &lt;code&gt;sheet-music.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open up &lt;code&gt;sheet-music.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, css, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, property, query } from &amp;quot;lit/decorators.js&amp;quot;;
import { IOSMDOptions, OpenSheetMusicDisplay } from &amp;quot;opensheetmusicdisplay&amp;quot;;

type BackendType = &amp;quot;svg&amp;quot; | &amp;quot;canvas&amp;quot;;
type DrawingType = &amp;quot;compact&amp;quot; | &amp;quot;default&amp;quot;;

@customElement(&amp;quot;sheet-music&amp;quot;)
export class SheetMusic extends LitElement {
  _zoom = 1.0;

  @property({ type: Boolean }) allowDrop = false;
  @property() src = &amp;quot;&amp;quot;;

  @query(&amp;quot;main&amp;quot;) canvas!: HTMLElement;

  controller?: OpenSheetMusicDisplay;
  options: IOSMDOptions = {
    autoResize: true,
    backend: &amp;quot;canvas&amp;quot; as BackendType,
    drawingParameters: &amp;quot;default&amp;quot; as DrawingType,
  };

  static styles = css`
    main {
      overflow-x: auto;
    }
  `;

  render() {
    return html`&amp;lt;main&amp;gt;&amp;lt;/main&amp;gt;`;
  }

  async renderMusic(content: string) {
    if (!this.controller) return;
    await this.controller.load(content);
    this.controller.zoom = this._zoom;
    this.controller.render();
    this.requestUpdate();
  }

  private async getMusic(): Promise&amp;lt;string&amp;gt; {
    // Check if src attribute is set and prefer it over the slot
    if (this.src.length &amp;gt; 0) return fetch(this.src).then((res) =&amp;gt; res.text());

    // Check if slot children exist and return the xml
    const elem = this.parentElement?.querySelector(
      &apos;script[type=&amp;quot;text/xml&amp;quot;]&apos;
    ) as HTMLScriptElement;
    if (elem) return elem.innerHTML;

    // Return nothing if neither is found
    return &amp;quot;&amp;quot;;
  }

  async firstUpdated() {
    this.controller = new OpenSheetMusicDisplay(this.canvas, this.options);
    this.requestUpdate();

    // Check for any music and update if found
    const music = await this.getMusic();
    if (music.length &amp;gt; 0) this.renderMusic(music);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    &amp;quot;sheet-music&amp;quot;: SheetMusic;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run &lt;code&gt;npm run dev&lt;/code&gt; and the following should appear if all went well:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/s_1_4wd2mls47h.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to learn more about building with Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The source for this example can be found &lt;a href=&quot;https://github.com/rodydavis/lit-sheet-music&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/s_2_mkoqbtpadj.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</content:encoded></item><item><title>Lit and VSCode Extensions</title><link>https://rodydavis.com/lit/vscode-extension/</link><guid isPermaLink="true">https://rodydavis.com/lit/vscode-extension/</guid><description>Create a VSCode extension using Lit web components with Vite, TypeScript, and Rollup, following a step-by-step guide.</description><pubDate>Mon, 20 Jan 2025 02:41:17 GMT</pubDate><content:encoded>&lt;h1&gt;Lit and VSCode Extensions&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to set up a &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; web component and use it to create a VSCode extension.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; You can find the final source &lt;a href=&quot;https://github.com/rodydavis/lit-vscode-extension&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Node &amp;gt;= 16&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by navigating in terminal to the location of the project and run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init @vitejs/app --template lit-ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enter a project name &lt;code&gt;lit-vscode-extension&lt;/code&gt; and now open the project in vscode and install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd lit-vscode-extension
npm i -D @types/node
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;vite.config.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from &amp;quot;vite&amp;quot;;
import { resolve } from &amp;quot;path&amp;quot;;

export default defineConfig({
  base: &amp;quot;/lit-vscode-extension/&amp;quot;,
  build: {
    outDir: &amp;quot;build&amp;quot;,
    rollupOptions: {
      external: /^vscode/,
      input: {
        main: resolve(__dirname, &amp;quot;index.html&amp;quot;),
      },
      output: {
        entryFileNames: &amp;quot;[name].js&amp;quot;,
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;package.json&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &amp;quot;name&amp;quot;: &amp;quot;lit-vscode-extension&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;Lit VSCode Extension Example&amp;quot;,
  &amp;quot;version&amp;quot;: &amp;quot;0.0.1&amp;quot;,
  &amp;quot;publisher&amp;quot;: &amp;quot;rodydavis&amp;quot;,
  &amp;quot;private&amp;quot;: true,
  &amp;quot;license&amp;quot;: &amp;quot;MIT&amp;quot;,
  &amp;quot;repository&amp;quot;: {
    &amp;quot;type&amp;quot;: &amp;quot;git&amp;quot;,
    &amp;quot;url&amp;quot;: &amp;quot;https://github.com/rodydavis/lit-vscode-extension&amp;quot;
  },
  &amp;quot;engines&amp;quot;: {
    &amp;quot;vscode&amp;quot;: &amp;quot;^1.47.0&amp;quot;
  },
  &amp;quot;categories&amp;quot;: [
    &amp;quot;Other&amp;quot;
  ],
  &amp;quot;activationEvents&amp;quot;: [
    &amp;quot;onCommand:lit.start&amp;quot;,
    &amp;quot;onCommand:lit.reset&amp;quot;,
    &amp;quot;onWebviewPanel:lit&amp;quot;
  ],
  &amp;quot;main&amp;quot;: &amp;quot;./build/extension.js&amp;quot;,
  &amp;quot;contributes&amp;quot;: {
    &amp;quot;commands&amp;quot;: [
      {
        &amp;quot;command&amp;quot;: &amp;quot;lit.start&amp;quot;,
        &amp;quot;title&amp;quot;: &amp;quot;Open Plugin&amp;quot;,
        &amp;quot;category&amp;quot;: &amp;quot;lit&amp;quot;
      },
      {
        &amp;quot;command&amp;quot;: &amp;quot;lit.reset&amp;quot;,
        &amp;quot;title&amp;quot;: &amp;quot;Reset&amp;quot;,
        &amp;quot;category&amp;quot;: &amp;quot;lit&amp;quot;
      }
    ]
  },
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;dev&amp;quot;: &amp;quot;vite&amp;quot;,
    &amp;quot;build&amp;quot;: &amp;quot;tsc &amp;amp;&amp;amp; vite build&amp;quot;,
    &amp;quot;ext&amp;quot;: &amp;quot;tsc src/extension.ts --outdir build --skipLibCheck --module commonjs&amp;quot;,
    &amp;quot;compile&amp;quot;: &amp;quot;npm run build &amp;amp;&amp;amp; npm run ext&amp;quot;,
    &amp;quot;serve&amp;quot;: &amp;quot;vite preview&amp;quot;
  },
  &amp;quot;dependencies&amp;quot;: {
    &amp;quot;lit&amp;quot;: &amp;quot;^2.0.0-rc.2&amp;quot;
  },
  &amp;quot;devDependencies&amp;quot;: {
    &amp;quot;@types/node&amp;quot;: &amp;quot;^15.12.4&amp;quot;,
    &amp;quot;@types/vscode&amp;quot;: &amp;quot;^1.57.0&amp;quot;,
    &amp;quot;typescript&amp;quot;: &amp;quot;^4.2.3&amp;quot;,
    &amp;quot;vite&amp;quot;: &amp;quot;^2.3.5&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the &lt;code&gt;package.json&lt;/code&gt; is updated run &lt;code&gt;npm i&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Template&lt;/h2&gt;
&lt;p&gt;Open up the &lt;code&gt;index.html&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/src/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;Lit Example&amp;lt;/title&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/my-element.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;my-element&amp;gt;
      &amp;lt;p&amp;gt;This is child content&amp;lt;/p&amp;gt;
    &amp;lt;/my-element&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Web Component&lt;/h2&gt;
&lt;p&gt;Open up &lt;code&gt;src/my-element.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html, css, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, property, state } from &amp;quot;lit/decorators.js&amp;quot;;

@customElement(&amp;quot;my-element&amp;quot;)
export class MyElement extends LitElement {
  static styles = css`
    :host {
      display: block;
      border: solid 1px gray;
      padding: 16px;
      max-width: 800px;
    }
  `;

  @property() name = &amp;quot;World&amp;quot;;
  @state() count = 0;

  render() {
    return html`
      &amp;lt;h1&amp;gt;Hello, ${this.name}!&amp;lt;/h1&amp;gt;
      &amp;lt;button @click=${() =&amp;gt; this.modify(1)} part=&amp;quot;button&amp;quot;&amp;gt;
        Click Count: ${this.count}
      &amp;lt;/button&amp;gt;
      &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
    `;
  }

  modify(val: number) {
    this.count += val;
  }

  reset() {
    this.count = 0;
  }

  async firstUpdated() {
    window.addEventListener(
      &amp;quot;message&amp;quot;,
      (e: any) =&amp;gt; {
        const message = e.data;
        const { command } = message;
        if (command === &amp;quot;reset&amp;quot;) {
          this.reset();
        }
      },
      false
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are just modifying the example to include a message listener for communicating with the vscode extension, methods for updating the count, and updating the render method.&lt;/p&gt;
&lt;p&gt;VSCode communicates with the plugin via post messages because the UI will be loaded in an &lt;code&gt;iframe&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now let&apos;s write the extension code in &lt;code&gt;src/extension.ts&lt;/code&gt;. First start by adding top level declarations that will be referenced multiple times.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import * as vscode from &amp;quot;vscode&amp;quot;;

const WEB_DIR: string = &amp;quot;build&amp;quot;;
const WEB_SCRIPT: string = &amp;quot;main.js&amp;quot;;
const TITLE: string = &amp;quot;Lit Example&amp;quot;;
const TAG: string = &amp;quot;my-element&amp;quot;;


const possible = [
  &amp;quot;ABCDEFGHIJKLMNOPQRSTUVWXYZ&amp;quot;,
  &amp;quot;abcdefghijklmnopqrstuvwxyz&amp;quot;,
  &amp;quot;0123456789&amp;quot;,
].join(&amp;quot;&amp;quot;);

function getNonce() {
  let text = &amp;quot;&amp;quot;;
  for (let i = 0; i &amp;lt; 32; i++) {
    const char = possible.charAt(Math.floor(Math.random() * possible.length));
    text += char;
  }
  return text;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also are creating a &lt;code&gt;getNonce&lt;/code&gt; method that will be used for making sure the scripts we load are the ones we passed in.&lt;/p&gt;
&lt;p&gt;Now create a &lt;code&gt;Panel&lt;/code&gt; that will contain the ui for our plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Panel {
  public static currentPanel: Panel | undefined;
  public static readonly viewType = &amp;quot;litExample&amp;quot;;
  private _disposables: vscode.Disposable[] = [];

  public static createOrShow(extensionUri: vscode.Uri) {
    const column = vscode.window.activeTextEditor
      ? vscode.window.activeTextEditor.viewColumn
      : undefined;
    if (Panel.currentPanel) {
      Panel.currentPanel.panel.reveal(column);
      return;
    }
    const panel = vscode.window.createWebviewPanel(
      Panel.viewType,
      TITLE,
      column || vscode.ViewColumn.One,
      getWebviewOptions(extensionUri)
    );
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  private constructor(
    public readonly panel: vscode.WebviewPanel,
    public readonly extensionUri: vscode.Uri
  ) {
    this._update();
    this.panel.onDidDispose(() =&amp;gt; this.dispose(), null, this._disposables);
    this.panel.onDidChangeViewState(
      (_) =&amp;gt; {
        if (this.panel.visible) {
          this._update();
        }
      },
      null,
      this._disposables
    );
    this.panel.webview.onDidReceiveMessage(
      (message) =&amp;gt; {
        switch (message.command) {
          case &amp;quot;alert&amp;quot;:
            vscode.window.showErrorMessage(message.text);
            return;
        }
      },
      null,
      this._disposables
    );
  }

  public sendMessage(command: string) {
    this.panel.webview.postMessage({ command: command });
  }

  public dispose() {
    Panel.currentPanel = undefined;
    this.panel.dispose();
    while (this._disposables.length) {
      const x = this._disposables.pop();
      if (x) {
        x.dispose();
      }
    }
  }

  private _update() {
    const webview = this.panel.webview;
    webview.html = this._getHtmlForWebview(webview);
  }

  private _getHtmlForWebview(webview: vscode.Webview) {
    const scriptPathOnDisk = vscode.Uri.joinPath(
      this.extensionUri,
      WEB_DIR,
      WEB_SCRIPT
    );
    const scriptUri = webview.asWebviewUri(scriptPathOnDisk);
    const nonce = getNonce();

    const slot = &amp;quot;&amp;lt;p&amp;gt;This is child content&amp;lt;/p&amp;gt;&amp;quot;;

    return `&amp;lt;!DOCTYPE html&amp;gt;
			&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
        &amp;lt;head&amp;gt;
          &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;
          &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;
          &amp;lt;title&amp;gt;${TITLE}&amp;lt;/title&amp;gt;
        &amp;lt;/head&amp;gt;
        &amp;lt;body class=&amp;quot;vscode-light&amp;quot;&amp;gt;
          &amp;lt;${TAG} nonce=&amp;quot;${nonce}&amp;quot; &amp;gt;
            ${slot}
          &amp;lt;/${TAG}&amp;gt;
          &amp;lt;script nonce=&amp;quot;${nonce}&amp;quot; type=&amp;quot;module&amp;quot; src=&amp;quot;${scriptUri}&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
        &amp;lt;/body&amp;gt;
			&amp;lt;/html&amp;gt;`;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Notice how we are recreating the html and not using the &lt;code&gt;index.html&lt;/code&gt;. This allows us to use the component for a deployed website but also the extension with separate app logic code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Everything else is just boilerplate for managing the panel state and disposing when it is finished.&lt;/p&gt;
&lt;p&gt;Now we can add the methods for loading our plugin and listening for the commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand(&amp;quot;lit.start&amp;quot;, () =&amp;gt; {
      Panel.createOrShow(context.extensionUri);
    })
  );

  context.subscriptions.push(
    vscode.commands.registerCommand(&amp;quot;lit.reset&amp;quot;, () =&amp;gt; {
      if (Panel.currentPanel) {
        Panel.currentPanel.sendMessage(&amp;quot;reset&amp;quot;);
      }
    })
  );

  if (vscode.window.registerWebviewPanelSerializer) {
    vscode.window.registerWebviewPanelSerializer(Panel.viewType, {
      async deserializeWebviewPanel(
        webviewPanel: vscode.WebviewPanel,
        state: any
      ) {
        console.log(`Received state: ${state}`);
        webviewPanel.webview.options = getWebviewOptions(context.extensionUri);
        Panel.revive(webviewPanel, context.extensionUri);
      },
    });
  }
}

function getWebviewOptions(extensionUri: vscode.Uri): vscode.WebviewOptions {
  return {
    enableScripts: true,
    localResourceRoots: [vscode.Uri.joinPath(extensionUri, WEB_DIR)],
  };
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are loading the extension and passing messages when the &lt;code&gt;lit.reset&lt;/code&gt; command. We are also returning the &lt;code&gt;getWebviewOptions&lt;/code&gt; options which &lt;code&gt;enableScripts&lt;/code&gt; is set to &lt;code&gt;true&lt;/code&gt; so we can run the injected js.&lt;/p&gt;
&lt;p&gt;The final code should look like the follow:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import * as vscode from &amp;quot;vscode&amp;quot;;

const WEB_DIR: string = &amp;quot;build&amp;quot;;
const WEB_SCRIPT: string = &amp;quot;main.js&amp;quot;;
const TITLE: string = &amp;quot;Lit Example&amp;quot;;
const TAG: string = &amp;quot;my-element&amp;quot;;

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand(&amp;quot;lit.start&amp;quot;, () =&amp;gt; {
      Panel.createOrShow(context.extensionUri);
    })
  );

  context.subscriptions.push(
    vscode.commands.registerCommand(&amp;quot;lit.reset&amp;quot;, () =&amp;gt; {
      if (Panel.currentPanel) {
        Panel.currentPanel.sendMessage(&amp;quot;reset&amp;quot;);
      }
    })
  );

  if (vscode.window.registerWebviewPanelSerializer) {
    vscode.window.registerWebviewPanelSerializer(Panel.viewType, {
      async deserializeWebviewPanel(
        webviewPanel: vscode.WebviewPanel,
        state: any
      ) {
        console.log(`Received state: ${state}`);
        webviewPanel.webview.options = getWebviewOptions(context.extensionUri);
        Panel.revive(webviewPanel, context.extensionUri);
      },
    });
  }
}

function getWebviewOptions(extensionUri: vscode.Uri): vscode.WebviewOptions {
  return {
    enableScripts: true,
    localResourceRoots: [vscode.Uri.joinPath(extensionUri, WEB_DIR)],
  };
}

class Panel {
  public static currentPanel: Panel | undefined;
  public static readonly viewType = &amp;quot;litExample&amp;quot;;
  private _disposables: vscode.Disposable[] = [];

  public static createOrShow(extensionUri: vscode.Uri) {
    const column = vscode.window.activeTextEditor
      ? vscode.window.activeTextEditor.viewColumn
      : undefined;
    if (Panel.currentPanel) {
      Panel.currentPanel.panel.reveal(column);
      return;
    }
    const panel = vscode.window.createWebviewPanel(
      Panel.viewType,
      TITLE,
      column || vscode.ViewColumn.One,
      getWebviewOptions(extensionUri)
    );
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  private constructor(
    public readonly panel: vscode.WebviewPanel,
    public readonly extensionUri: vscode.Uri
  ) {
    this._update();
    this.panel.onDidDispose(() =&amp;gt; this.dispose(), null, this._disposables);
    this.panel.onDidChangeViewState(
      (_) =&amp;gt; {
        if (this.panel.visible) {
          this._update();
        }
      },
      null,
      this._disposables
    );
    this.panel.webview.onDidReceiveMessage(
      (message) =&amp;gt; {
        switch (message.command) {
          case &amp;quot;alert&amp;quot;:
            vscode.window.showErrorMessage(message.text);
            return;
        }
      },
      null,
      this._disposables
    );
  }

  public sendMessage(command: string) {
    this.panel.webview.postMessage({ command: command });
  }

  public dispose() {
    Panel.currentPanel = undefined;
    this.panel.dispose();
    while (this._disposables.length) {
      const x = this._disposables.pop();
      if (x) {
        x.dispose();
      }
    }
  }

  private _update() {
    const webview = this.panel.webview;
    webview.html = this._getHtmlForWebview(webview);
  }

  private _getHtmlForWebview(webview: vscode.Webview) {
    const scriptPathOnDisk = vscode.Uri.joinPath(
      this.extensionUri,
      WEB_DIR,
      WEB_SCRIPT
    );
    const scriptUri = webview.asWebviewUri(scriptPathOnDisk);
    const nonce = getNonce();

    const slot = &amp;quot;&amp;lt;p&amp;gt;This is child content&amp;lt;/p&amp;gt;&amp;quot;;

    const htmlSource = `
    &amp;lt;!DOCTYPE html&amp;gt;
    &amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
      &amp;lt;head&amp;gt;
        &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;
        &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;
        &amp;lt;title&amp;gt;${TITLE}&amp;lt;/title&amp;gt;
      &amp;lt;/head&amp;gt;
      &amp;lt;body class=&amp;quot;vscode-light&amp;quot;&amp;gt;
        &amp;lt;${TAG} nonce=&amp;quot;${nonce}&amp;quot; &amp;gt;
          ${slot}
        &amp;lt;/${TAG}&amp;gt;
        &amp;lt;script nonce=&amp;quot;${nonce}&amp;quot; type=&amp;quot;module&amp;quot; src=&amp;quot;${scriptUri}&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;`;

    return htmlSource;
  }
}

const possible = [
  &amp;quot;ABCDEFGHIJKLMNOPQRSTUVWXYZ&amp;quot;,
  &amp;quot;abcdefghijklmnopqrstuvwxyz&amp;quot;,
  &amp;quot;0123456789&amp;quot;,
].join(&amp;quot;&amp;quot;);

function getNonce() {
  let text = &amp;quot;&amp;quot;;
  for (let i = 0; i &amp;lt; 32; i++) {
    const char = possible.charAt(Math.floor(Math.random() * possible.length));
    text += char;
  }
  return text;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Running&lt;/h2&gt;
&lt;p&gt;Make sure to install the dependencies by running &lt;code&gt;npm i&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To build the extension run &lt;code&gt;npm run compile&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To open the extension and debug hit &lt;code&gt;F5&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/v_1_lcs00w2hlu.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;To run the commands to open the extension run &lt;code&gt;lit: open plugin&lt;/code&gt; or &lt;code&gt;lit: reset&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/v_2_g5d9uzk1if.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;To debug the extension when it is open run &lt;code&gt;Developer: Open Webview Developer Tools&lt;/code&gt; from the command pallet.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/v_3_djp3kxusi7.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you want to learn more about building a vscode extension you can read more &lt;a href=&quot;https://code.visualstudio.com/api&quot;&gt;here&lt;/a&gt; and for Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>The Perfect Brisket</title><link>https://rodydavis.com/recipes/the-perfect-brisket/</link><guid isPermaLink="true">https://rodydavis.com/recipes/the-perfect-brisket/</guid><description>A detailed guide for smoking a 13-16lb beef brisket, including sourcing, preparation, trimming, and seasoning instructions for a flavorful, large-batch meal.</description><pubDate>Sun, 19 Jan 2025 02:57:22 GMT</pubDate><content:encoded>&lt;h1&gt;The Perfect Brisket&lt;/h1&gt;
&lt;p&gt;This is a living document meant to be updated each time I improve on the perfect smoked brisket recipe. I don&apos;t get to cook it often but when I do I go all out.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Be prepared to take 4-6hrs to smoke the meat&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Ingredients&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;13-16lb Whole Beef Brisket&lt;/li&gt;
&lt;li&gt;Classic Yellow Mustard&lt;/li&gt;
&lt;li&gt;Dry Rub (Mr. P&apos;s Rub-a-butt)&lt;/li&gt;
&lt;li&gt;Large Cutting Board&lt;/li&gt;
&lt;li&gt;Very Sharp Knife&lt;/li&gt;
&lt;li&gt;Smoker (Akorn)&lt;/li&gt;
&lt;li&gt;Apple Wood Chips&lt;/li&gt;
&lt;li&gt;Charcoal Briquettes&lt;/li&gt;
&lt;li&gt;Meat Thermometer&lt;/li&gt;
&lt;li&gt;Aluminum Foil&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_1_eso97ppzim.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;First you need to source the brisket. I usually like to go to a local butcher shop and look for some good deals. This last time I went it was at $6.99/lb but I have gotten it as low as $3.99/lb. To cook for a big family and have leftovers you get get a 13-16lb brisket (This is usually what they already have in stock).&lt;/p&gt;
&lt;h2&gt;Prepare The Meat&lt;/h2&gt;
&lt;p&gt;After you have the meat out of the packaging you can place it on a cutting board and start to pat dry the meat. This will take quite a few paper towels to dry all the blood and moisture.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_2_73r2h4ft7u.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;After the meat is dry you can grab a very sharp curved knife to be used to trim the fat. The goal of the trimming is to remove most of the hard fat in the thick areas.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_3_e7wg250gea.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You shouldn&apos;t have to trim that much unless you are preparing for a competition but in this case I am preparing for a group of people and need more meat. As you are trimming the fat you want to make sure to only cut the thick hard parts and not into the meat (it&apos;s ok if this is not perfect, you can use the trimmings for other stuff).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_4_lkvv57zpw1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Season The Meat&lt;/h2&gt;
&lt;p&gt;With the meat trimmed it is now time to put the dry rub on the brisket. You will need to grab the mustard and whichever seasoning you chose. Be prepared to get very messy at this step, and id you have a helping hand that would be even better (Gloves also work).&lt;/p&gt;
&lt;p&gt;Start of with lines of mustard on the meat so that you have a nice even coat to rub in. Don&apos;t worry if you do not like mustard like myself, it will cook out and is only used to keep the seasoning on the meat.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_5_jol8llekzu.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now add the seasoning and rub it in with your hands or barbecue brush and repeat on the opposite side. You want to get a nice thick coat.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_6_n1a3i4ibsw.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;After you finish the meat make sure you leave it out for 20 min to bring it up to room temperature while you prepare the grill.&lt;/p&gt;
&lt;h2&gt;Prepare The Smoker&lt;/h2&gt;
&lt;p&gt;While the prepared meat is sitting you will need to prepare the grill. You need to light the coals but not all of them. I like to like the center of them and then control the airflow. Keep in mind you will need to keep the smoker under 250F throughout the duration of the cook.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_7_33rbpf4lhk.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Another thing I like to use is a diffuser to provide indirect heat and allow just the smoke to cook it. If you do not have this it is not a problem. You can also add the apple wood chips inside a aluminum pouch at this time.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_8_nk5r86f3ky.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;This may take about 20-30 min to bring the grill up to the right temperature before putting the meat on.&lt;/p&gt;
&lt;h2&gt;Cook the Meat&lt;/h2&gt;
&lt;p&gt;Now that the meat is room temperature and the grill is warm or close to the right temperature, grab the meat and place it on the smoker.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_9_r4vpfo1tjd.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Once you have placed the meat on the grate you can put in the temperature sensor sideways in the thickest side of the meat.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_10_9ynpsv0gg1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now comes the fun part... waiting. You will need to come out every 30-60 min to make sure the meat is not getting too hot/cold and make micro adjustments to the vents.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_11_pu24vfrh4u.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The internal temperature you are shooting for is 190F and it will take awhile for it to come up. When the meat is at 150F or it&apos;s been 3 hours and the skin is getting brown, you can wrap the meat in foil to lock in the juices.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_12_6bc97byurs.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The foil should cover all sides of the meat, and around this time you can add more wood chips to increase the smoke.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_13_6qxv0y27nt.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Enjoy&lt;/h2&gt;
&lt;p&gt;Regardless of how it turned out it should still smell amazing and taste even better. If you made sure to come check on it often and not let it get to hot it should still be nice and juicy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/brisket_14_exz9yiawi9.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;It will be really tempting to eat it right away, but you need to let the meat rest for 20 min before eating.&lt;/p&gt;
&lt;p&gt;Let me know how it turned out by mentioning me on &lt;a href=&quot;https://twitter.com/rodydavis&quot;&gt;twitter&lt;/a&gt; or &lt;a href=&quot;https://instagram.com/rodydavisjr&quot;&gt;instagram&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>How to do Full Text Search with SQLite</title><link>https://rodydavis.com/sqlite/fts5/</link><guid isPermaLink="true">https://rodydavis.com/sqlite/fts5/</guid><description>Learn how to implement full-text search in SQLite using the fts5 extension by creating virtual tables and keeping the index synchronized with your data.</description><pubDate>Sat, 18 Jan 2025 20:49:31 GMT</pubDate><content:encoded>&lt;h1&gt;How to do Full Text Search with SQLite&lt;/h1&gt;
&lt;p&gt;SQLite has a powerful way to add new functionality via &lt;a href=&quot;https://www.sqlite.org/loadext.html&quot;&gt;loadable extensions&lt;/a&gt;. The first-party ones include &lt;a href=&quot;https://www.sqlite.org/fts5.html&quot;&gt;fts5&lt;/a&gt;, &lt;a href=&quot;https://www.sqlite.org/json1.html&quot;&gt;json1&lt;/a&gt; and a couple others.&lt;/p&gt;
&lt;p&gt;When building applications it is common to add searching features based on data coming from tables and you may already have queries for fuzzy searching with &lt;strong&gt;LIKE&lt;/strong&gt;. You may be excited to hear that SQLite can easily add fully query capabilities over a dataset all with just a simple &lt;strong&gt;MATCH&lt;/strong&gt; keyword. 👀&lt;/p&gt;
&lt;h2&gt;Creating your first search index&lt;/h2&gt;
&lt;p&gt;Full text search in SQLite requires storing the index in &lt;strong&gt;VIRTUAL&lt;/strong&gt; tables, which allow for optimized storage of the index based on the queries we will execute against it.&lt;/p&gt;
&lt;p&gt;You can create the virtual table for the index making sure to include the &lt;strong&gt;USING&lt;/strong&gt; directive for the fts5 target.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE VIRTUAL TABLE posts_fts USING fts5 (
    title,
    description,
    content,
    content=posts,
    content_rowid=id
);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Text IDs are also supported instead of just INTEGERS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a standard callout. You can customize its content and even the icon.&lt;/p&gt;
&lt;h3&gt;Contentless tables&lt;/h3&gt;
&lt;p&gt;You can also create a contentless table that will not be based on any existing tables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE VIRTUAL TABLE example_fts USING fts5 (
    name,
    description,
    content=&apos;&apos;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Keeping the index up to date&lt;/h2&gt;
&lt;p&gt;By having the source content be stored in another table we need to make sure to keep both tables in sync and avoid updating the index in a hot path when trying to make a query.&lt;/p&gt;
&lt;p&gt;By default when you create table it will be empty, even if the source table is populated. You do have various options for populating the index.&lt;/p&gt;
&lt;h3&gt;Update by query&lt;/h3&gt;
&lt;p&gt;If you use a contentless table or want to pull in data from a view you can update by query.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO posts_fts (id, title, description, content)
SELECT id, title, description, content FROM posts;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rebuild command&lt;/h3&gt;
&lt;p&gt;Using the rebuild command it will update the index based on the content table specified.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO posts_fts(posts_fts) VALUES(&apos;rebuild&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Triggers&lt;/h3&gt;
&lt;p&gt;We can use SQLite triggers to automatically keep the records updated:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TRIGGER posts_insert AFTER INSERT ON posts BEGIN
  INSERT INTO posts_fts(id, title, description, content)
  VALUES (new.id, new.title, new.description, new.content);
END;

CREATE TRIGGER posts_delete AFTER DELETE ON posts BEGIN
  INSERT INTO posts_fts(posts_fts, id, title, description, content)
  VALUES (&apos;delete&apos;, old.id, old.title, old.description, old.content);
END;

CREATE TRIGGER posts_update AFTER UPDATE ON posts BEGIN
  INSERT INTO posts_fts(posts_fts, id, title, description, content)
  VALUES (&apos;delete&apos;, old.id, old.title, old.description, old.content);
  INSERT INTO posts_fts(id, title, description, content)
  VALUES (new.id, new.title, new.description, new.content);
END;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will always ensure the two tables are in sync for any CRUD actions on the source table.&lt;/p&gt;
&lt;h2&gt;Searching the index&lt;/h2&gt;
&lt;h3&gt;Query syntax&lt;/h3&gt;
&lt;p&gt;Here is the supported query syntax:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;lt;phrase&amp;gt;    := string [*]
&amp;lt;phrase&amp;gt;    := &amp;lt;phrase&amp;gt; + &amp;lt;phrase&amp;gt;
&amp;lt;neargroup&amp;gt; := NEAR ( &amp;lt;phrase&amp;gt; &amp;lt;phrase&amp;gt; ... [, N] )
&amp;lt;query&amp;gt;     := [ [-] &amp;lt;colspec&amp;gt; :] [^] &amp;lt;phrase&amp;gt;
&amp;lt;query&amp;gt;     := [ [-] &amp;lt;colspec&amp;gt; :] &amp;lt;neargroup&amp;gt;
&amp;lt;query&amp;gt;     := [ [-] &amp;lt;colspec&amp;gt; :] ( &amp;lt;query&amp;gt; )
&amp;lt;query&amp;gt;     := &amp;lt;query&amp;gt; AND &amp;lt;query&amp;gt;
&amp;lt;query&amp;gt;     := &amp;lt;query&amp;gt; OR &amp;lt;query&amp;gt;
&amp;lt;query&amp;gt;     := &amp;lt;query&amp;gt; NOT &amp;lt;query&amp;gt;
&amp;lt;colspec&amp;gt;   := colname
&amp;lt;colspec&amp;gt;   := { colname1 colname2 ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To preform an actual query on the index we will need to use the &lt;strong&gt;MATCH&lt;/strong&gt; keyword and order by the rank.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT posts.* FROM posts_fts
INNER JOIN posts ON posts.id = posts_fts.rowid
WHERE posts_fts MATCH :query
ORDER BY rank;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sqlite.org/fts5.html&quot;&gt;https://www.sqlite.org/fts5.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.datasette.io/en/latest/full_text_search.html&quot;&gt;https://docs.datasette.io/en/latest/full_text_search.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Using SQLite as a Key Value Store</title><link>https://rodydavis.com/sqlite/key-value/</link><guid isPermaLink="true">https://rodydavis.com/sqlite/key-value/</guid><description>Use SQLite as a key-value store to efficiently manage settings and non-relational data by creating a table, saving, reading, deleting, and searching values using SQL queries.</description><pubDate>Fri, 17 Jan 2025 06:54:23 GMT</pubDate><content:encoded>&lt;h1&gt;Using SQLite as a Key Value Store&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sqlite.org/&quot;&gt;SQLite&lt;/a&gt; is a very capable edge database that can store various shapes of data.&lt;/p&gt;
&lt;p&gt;Key/Value databases are popular in applications for storing settings, and other non-relational data.&lt;/p&gt;
&lt;p&gt;By using SQLite to store the key/values you can contain all the data for a user in a single file and can &lt;a href=&quot;https://www.sqlite.org/lang_attach.html&quot;&gt;attach it to other databases&lt;/a&gt; or sync it to a server.&lt;/p&gt;
&lt;h2&gt;Create the table&lt;/h2&gt;
&lt;p&gt;To store key/value type data we need to first create our table.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE key_value (
  key TEXT NOT NULL PRIMARY KEY,
  value,
  UNIQUE(key)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;user_id&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;foo&lt;/td&gt;
&lt;td&gt;bar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;guest&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;SQLite has &lt;a href=&quot;https://www.sqlite.org/datatype3.html&quot;&gt;optional column types&lt;/a&gt; and can be very useful for dynamic values.&lt;/p&gt;
&lt;h2&gt;Save a value&lt;/h2&gt;
&lt;p&gt;To save a value for a given key we can run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT OR REPLACE 
INTO key_value (key, value) 
VALUES (:key, :value)
RETURNING *;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;user_id&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Since the key is &lt;a href=&quot;https://www.sqlitetutorial.net/sqlite-unique-constraint/&quot;&gt;UNIQUE&lt;/a&gt; we do not have to worry about conflicts as it will overwrite the value as intended.&lt;/p&gt;
&lt;h2&gt;Read a value&lt;/h2&gt;
&lt;p&gt;To read a value we can pass in a key to our query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT value FROM key_value 
WHERE key = :key;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;value&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;This will only return a single value column with a max of 1 rows.&lt;/p&gt;
&lt;h2&gt;Delete a value&lt;/h2&gt;
&lt;p&gt;To delete a value or key we can run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;DELETE FROM key_value 
WHERE key = :key;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Search for key or value&lt;/h2&gt;
&lt;p&gt;We can also search for a specific key or value (if it is a string) with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT key, value
FROM key_value 
WHERE key LIKE :query 
OR value LIKE :query;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bar&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;foo&lt;/td&gt;
&lt;td&gt;bar&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Drift Support&lt;/h2&gt;
&lt;p&gt;If you are using &lt;a href=&quot;https://drift.simonbinder.eu/&quot;&gt;Drift&lt;/a&gt; in dart, create a new file &lt;code&gt;key_value.drift&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE key_value (
  &amp;quot;key&amp;quot; TEXT NOT NULL PRIMARY KEY,
  value TEXT,
  UNIQUE(&amp;quot;key&amp;quot;)
);

setItem:
INSERT OR REPLACE 
INTO key_value (&amp;quot;key&amp;quot;, value) 
VALUES (:key, :value)
RETURNING *;

getItem:
SELECT value FROM key_value 
WHERE &amp;quot;key&amp;quot; = :key;

deleteItem:
DELETE FROM key_value 
WHERE &amp;quot;key&amp;quot; = :key;

searchItem:
SELECT &amp;quot;key&amp;quot;, value
FROM key_value 
WHERE &amp;quot;key&amp;quot; LIKE :query 
OR value LIKE :query;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>How to store SQLite as NoSQL Store</title><link>https://rodydavis.com/sqlite/no-sql/</link><guid isPermaLink="true">https://rodydavis.com/sqlite/no-sql/</guid><description>Store JSON data in SQLite by creating a table with a path, data (as a string), TTL, created, and updated columns, then use INSERT/REPLACE statements to save and retrieve JSON objects.</description><pubDate>Sat, 18 Jan 2025 06:24:22 GMT</pubDate><content:encoded>&lt;h1&gt;How to store SQLite as NoSQL Store&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sqlite.org/&quot;&gt;SQLite&lt;/a&gt; is a very capable edge database that can store various shapes of data.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.mongodb.com/nosql-explained#:~:text=Some%20say%20the%20term%20%E2%80%9CNoSQL,format%20other%20than%20relational%20tables.&quot;&gt;NoSQL databases&lt;/a&gt; are very popular due to the schema-less nature of storing of the data but it is totally possible to store these documents in SQLite.&lt;/p&gt;
&lt;p&gt;SQLite actually has great &lt;a href=&quot;https://www.sqlite.org/json1.html&quot;&gt;JSON support&lt;/a&gt; and even supports &lt;a href=&quot;https://sqlite.org/draft/jsonb.html&quot;&gt;JSONB&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Create the table&lt;/h2&gt;
&lt;p&gt;To store JSON documents we need to create a table to store the values as strings.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE documents (
  path TEXT NOT NULL PRIMARY KEY,
  data TEXT,
  ttl INTEGER,
  created INTEGER NOT NULL,
  updated INTEGER NOT NULL,
  UNIQUE(path)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;path&lt;/th&gt;
&lt;th&gt;data&lt;/th&gt;
&lt;th&gt;ttl&lt;/th&gt;
&lt;th&gt;created&lt;/th&gt;
&lt;th&gt;updated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/posts/1&lt;/td&gt;
&lt;td&gt;{&amp;quot;id&amp;quot;:1}&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/posts/2&lt;/td&gt;
&lt;td&gt;{&amp;quot;id&amp;quot;:2}&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/users/1&lt;/td&gt;
&lt;td&gt;{&amp;quot;id&amp;quot;:1}&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The basic idea is to store a JSON object and an unique path.&lt;/p&gt;
&lt;p&gt;There is an optional &lt;a href=&quot;https://www.cloudflare.com/learning/cdn/glossary/time-to-live-ttl/#:~:text=What%20is%20time%2Dto%2Dlive%20(TTL)%20in%20networking,CDN%20caching%20and%20DNS%20caching.&quot;&gt;TTL&lt;/a&gt; to automatically delete rows when they reach the stale date.&lt;/p&gt;
&lt;h2&gt;Save a document&lt;/h2&gt;
&lt;p&gt;To save a document we can encode our JSON as a string or binary and save in in the table with a unique path.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT OR REPLACE 
INTO documents (path, data, ttl, created, updated) 
VALUES (:path, :data, :ttl, :created, :updated)
RETURNING *;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also use JSON functions to save the Object to a valid JSON string.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT OR REPLACE 
INTO documents (path, data, ttl, created, updated) 
VALUES (&amp;quot;/posts/1&amp;quot;, json(&apos;{&amp;quot;id&amp;quot; 1}&apos;), NULL, 0, 0)
RETURNING *;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;path&lt;/th&gt;
&lt;th&gt;data&lt;/th&gt;
&lt;th&gt;ttl&lt;/th&gt;
&lt;th&gt;created&lt;/th&gt;
&lt;th&gt;updated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/posts/1&lt;/td&gt;
&lt;td&gt;{&amp;quot;id&amp;quot;:1}&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Reading a document&lt;/h2&gt;
&lt;p&gt;To read a document we just need the path. If a TTL is set we can &lt;a href=&quot;https://www.sqlite.org/lang_datefunc.html&quot;&gt;calculate if the current date&lt;/a&gt; is greater than the offset and not return the document.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM documents 
WHERE path = :path
AND (
	(ttl IS NOT NULL AND ttl + updated &amp;lt; unixepoch())
	OR
	ttl IS NULL
);
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;path&lt;/th&gt;
&lt;th&gt;data&lt;/th&gt;
&lt;th&gt;ttl&lt;/th&gt;
&lt;th&gt;created&lt;/th&gt;
&lt;th&gt;updated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/posts/1&lt;/td&gt;
&lt;td&gt;{&amp;quot;id&amp;quot;:1}&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Get documents for a collection&lt;/h2&gt;
&lt;p&gt;We can query all the docs for a given collection using some built-in functions and a path prefix:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM documents 
WHERE (
	path LIKE :prefix
	AND
	(LENGTH(path) - LENGTH(REPLACE(path, &apos;/&apos;, &apos;&apos;))) = (LENGTH(:prefix) - LENGTH(REPLACE(:prefix, &apos;/&apos;, &apos;&apos;)))
)
AND (
	(ttl IS NOT NULL AND ttl + updated &amp;lt; unixepoch())
	OR
	ttl IS NULL
)
ORDER BY created;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is expected to search for a :prefix with the &lt;code&gt;/%&lt;/code&gt; at the end:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;quot;/my/path/%&amp;quot; // search for /my/path&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Deleting expired documents&lt;/h2&gt;
&lt;p&gt;Using the TTL field we can delete all expired documents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;DELETE FROM documents
WHERE ttl IS NOT NULL
AND ttl + updated &amp;lt; unixepoch();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>SQLite on the UI Thread</title><link>https://rodydavis.com/sqlite/ui-thread/</link><guid isPermaLink="true">https://rodydavis.com/sqlite/ui-thread/</guid><description>SQLite offers efficient, synchronous database access within the UI thread, enabling fast data retrieval and seamless integration for UI-driven applications, even with large datasets.</description><pubDate>Sat, 18 Jan 2025 20:38:57 GMT</pubDate><content:encoded>&lt;h1&gt;SQLite on the UI Thread&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sqlite.org/&quot;&gt;SQLite&lt;/a&gt; is a lot faster than you may realize. In &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt; for example there is &lt;a href=&quot;https://pub.dev/packages/drift&quot;&gt;drift&lt;/a&gt;, &lt;a href=&quot;https://pub.dev/packages/sqlite_async&quot;&gt;sqlite_async&lt;/a&gt; and &lt;a href=&quot;https://pub.dev/packages/sqflite&quot;&gt;sqflite&lt;/a&gt; which allow for async access of data. But with &lt;a href=&quot;https://pub.dev/packages/sqlite3&quot;&gt;sqlite3&lt;/a&gt; you can query with sync functions! 🤯&lt;/p&gt;
&lt;p&gt;Here is a list view where there are 10000 items and each item is retrieved with a select statement 👀&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/demo_tobl81yuqz.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Source: &lt;a href=&quot;https://gist.github.com/rodydavis/4a6dca4a2e1afc530ac93e94a76a594a&quot;&gt;https://gist.github.com/rodydavis/4a6dca4a2e1afc530ac93e94a76a594a&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;SQLite, when used effectively, can be a powerful asset for UI-driven applications. Operating within the same process and thread as the UI, it offers a seamless integration that can significantly improve component building.&lt;/p&gt;
&lt;p&gt;Async/await does not mean you will be building the most performant applications, and in some cases will &lt;a href=&quot;https://madelinemiller.dev/blog/javascript-promise-overhead/&quot;&gt;incur a performance penalty&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Even with extensive datasets, SQLite demonstrates remarkable efficiency. Its ability to handle millions of rows without compromising speed is a testament to its robust architecture. Contrary to the misconception of being solely a background-thread database, SQLite functions as a process-level library, akin to any other C-based library.&lt;/p&gt;
&lt;p&gt;By strategically employing indexes and queries, developers can achieve nanosecond response times and mitigate N+1 query issues. The judicious use of views, indexes, and virtual tables is paramount in optimizing performance.&lt;/p&gt;
&lt;p&gt;Complex join operations and the ability to retrieve only essential data for display further underscore SQLite&apos;s versatility. For example, when presenting a list view or cards, SQLite can efficiently fetch the required 30 items without undue overhead.&lt;/p&gt;
&lt;p&gt;SQLite&apos;s flexibility extends beyond single-database scenarios. The &lt;a href=&quot;https://www.sqlite.org/lang_attach.html&quot;&gt;ATTACH&lt;/a&gt; feature enables the management of multiple databases within a single application. Additionally, the concept of isolates or workers allows for parallel processing, further enhancing performance and responsiveness.&lt;/p&gt;
&lt;p&gt;From simple &lt;a href=&quot;https://rodydavis.com/sqlite/key-value&quot;&gt;key-value&lt;/a&gt; stores to intricate data modeling, SQLite&apos;s capabilities are vast. By applying appropriate &lt;a href=&quot;https://www.sqlite.org/pragma.html&quot;&gt;PRAGMAs&lt;/a&gt;, such as WAL mode, developers can tailor SQLite&apos;s behavior to meet specific application requirements.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.reddit.com/r/rails/comments/16cbiz9/the_6_pragmas_you_need_to_know_to_tune_your/&quot;&gt;Example PRAGMA&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA journal_size_limit = 67108864;
PRAGMA mmap_size = 134217728;
PRAGMA cache_size = 2000;
PRAGMA busy_timeout = 5000;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Narobi National Park</title><link>https://rodydavis.com/travel/narobi-national-park/</link><guid isPermaLink="true">https://rodydavis.com/travel/narobi-national-park/</guid><description>Explore Nairobi National Park in Kenya, where wildlife like rhinos, giraffes, and hippos roam with the city skyline visible in the background, offering a unique and harmonious safari experience.</description><pubDate>Sun, 19 Jan 2025 03:23:49 GMT</pubDate><content:encoded>&lt;h1&gt;Narobi National Park&lt;/h1&gt;
&lt;p&gt;On a recent &lt;a href=&quot;https://flutter.dev/events/flutter-forward&quot;&gt;work trip&lt;/a&gt; to Nairobi, Kenya I was able to take a day trip to &lt;a href=&quot;http://www.kws.go.ke/parks/nairobi-national-park&quot;&gt;Narobi National Park&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What is so wild about this national park is the city is 100% visible from the part at all times. You can grab a pic of a Rhino with a sky scraper in the background. There is a sense of harmony and respect I really love about this place.&lt;/p&gt;
&lt;h2&gt;Rhinos&lt;/h2&gt;
&lt;p&gt;Something I have always wanted to see is &lt;a href=&quot;https://www.worldwildlife.org/species/white-rhino&quot;&gt;White Rhinos&lt;/a&gt;. On a trip to &lt;a href=&quot;https://www.krugerpark.co.za/&quot;&gt;Kruger National Park&lt;/a&gt; in South Africa I was not able to see them because of how protected they are. They would explicitly not track them on the game boards and had men with guns to protect them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/n_1_9ekddhd4j3.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;In addition to the adults there were also babies. They were sleeps so peacefully. I was able to get a few shots of them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/n_2_jduubmzdxj.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Giraffes&lt;/h2&gt;
&lt;p&gt;Giraffes are an animal you probably get to see a lot in the zoo, but when seeing it in the wild it still is incredible. They travel together and you can pick them out from the trees. They are so tall and the adults can get up to 18 feet tall.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/n_3_dg8gghux4x.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Later that day we got to see the giraffes at the &lt;a href=&quot;https://www.giraffecentre.org/&quot;&gt;Giraffe Center&lt;/a&gt;. It was a really cool experience. You can feed them from a platform and they will come up and eat from your hand. It was a really cool experience. The tongue is so long and feels like sand paper.&lt;/p&gt;
&lt;h2&gt;Hippos&lt;/h2&gt;
&lt;p&gt;Hippos are so powerful and can be very dangerous. It&apos;s easy to forget that they are animals and not just statues. While they all lay in the water together, they are still very territorial and will attack if you get too close.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/h_4_exp3tn4or8.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Impalas&lt;/h2&gt;
&lt;p&gt;The impalas are so cute and are the most common animal in the park. They are very fast and can jump up to 10 feet in the air.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/h_5_ugjq51azle.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Water Buffalo&lt;/h2&gt;
&lt;p&gt;The water buffalo are so cool. They are so big and can weigh up to 2,000 pounds. We got to see many skulls and feel the horns which are so dense.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/h_6_vrdg7nb4hy.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Zebras&lt;/h2&gt;
&lt;p&gt;Zebras are something that is also common to see in zoos but in the wild they are so cool. We saw so many of them in the park and quite a few babies.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/n_7_4pac9s40u1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Location&lt;/h2&gt;
</content:encoded></item><item><title>File Based Routing for Static Sites</title><link>https://rodydavis.com/web/file-based-routing/</link><guid isPermaLink="true">https://rodydavis.com/web/file-based-routing/</guid><description>Learn how to build a static site with file-based routing using TypeScript, including project setup, configuration, and deployment with WebDevServer.</description><pubDate>Sun, 19 Jan 2025 23:59:39 GMT</pubDate><content:encoded>&lt;h1&gt;File Based Routing for Static Sites&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to use file based routing to output as a static site multi page application.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/static-site-file-based-routing&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/static-site-file-based-routing/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Step 1&lt;/h2&gt;
&lt;p&gt;Create a new folder called “static-site-file-based-routing” and open it up in VSCode.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;mkdir static-site-file-based-routing
cd static-site-file-based-routing
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2&lt;/h2&gt;
&lt;p&gt;Create a &lt;code&gt;tsconfig.json&lt;/code&gt; and replace it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &amp;quot;compilerOptions&amp;quot;: {
    &amp;quot;incremental&amp;quot;: true,
    &amp;quot;target&amp;quot;: &amp;quot;es5&amp;quot;,
    &amp;quot;module&amp;quot;: &amp;quot;es2020&amp;quot;,
    &amp;quot;outDir&amp;quot;: &amp;quot;dist&amp;quot;,
    &amp;quot;strict&amp;quot;: true,
    &amp;quot;moduleResolution&amp;quot;: &amp;quot;node&amp;quot;,
    &amp;quot;esModuleInterop&amp;quot;: true,
    &amp;quot;skipLibCheck&amp;quot;: true,
    &amp;quot;forceConsistentCasingInFileNames&amp;quot;: true,
    &amp;quot;typeRoots&amp;quot;: [
      &amp;quot;node_modules/@types&amp;quot;,
      &amp;quot;src/@types&amp;quot;
    ]
  },
  &amp;quot;include&amp;quot;: [
    &amp;quot;src/**/*&amp;quot;
  ],
  &amp;quot;exclude&amp;quot;: [
    &amp;quot;node_modules&amp;quot;,
    &amp;quot;dist&amp;quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3&lt;/h2&gt;
&lt;p&gt;Create a &lt;code&gt;package.json&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &amp;quot;name&amp;quot;: &amp;quot;static-site-file-based-routing&amp;quot;,
  &amp;quot;version&amp;quot;: &amp;quot;1.0.0&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;File based routing for HTML MPA&amp;quot;,
  &amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;,
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;start&amp;quot;: &amp;quot;wds --node-resolve --root-dir build --base-path /static-site-file-based-routing --open --watch&amp;quot;,
    &amp;quot;postinstall&amp;quot;: &amp;quot;npm run tsc&amp;quot;,
    &amp;quot;build&amp;quot;: &amp;quot;node dist/main.js --inputDir ./example --outputDir ./build&amp;quot;,
    &amp;quot;dev&amp;quot;: &amp;quot;node dist/main.js --inputDir ./example --outputDir ./build -w&amp;quot;,
    &amp;quot;tsc&amp;quot;: &amp;quot;tsc&amp;quot;,
    &amp;quot;tsc:watch&amp;quot;: &amp;quot;tsc -w&amp;quot;
  },
  &amp;quot;devDependencies&amp;quot;: {
    &amp;quot;@pkgjs/parseargs&amp;quot;: &amp;quot;^0.10.0&amp;quot;,
    &amp;quot;@types/markdown-it&amp;quot;: &amp;quot;^12.2.3&amp;quot;,
    &amp;quot;@types/node&amp;quot;: &amp;quot;^18.7.23&amp;quot;,
    &amp;quot;@web/dev-server&amp;quot;: &amp;quot;^0.1.34&amp;quot;,
    &amp;quot;chokidar&amp;quot;: &amp;quot;^3.5.3&amp;quot;,
    &amp;quot;highlight.js&amp;quot;: &amp;quot;^11.6.0&amp;quot;,
    &amp;quot;html-format&amp;quot;: &amp;quot;^1.0.2&amp;quot;,
    &amp;quot;markdown-it&amp;quot;: &amp;quot;^13.0.1&amp;quot;,
    &amp;quot;parse5&amp;quot;: &amp;quot;^7.1.1&amp;quot;,
    &amp;quot;typescript&amp;quot;: &amp;quot;^4.8.4&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run &lt;code&gt;npm install&lt;/code&gt; to install all the dependencies.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I am using &lt;code&gt;@web/dev-server&lt;/code&gt; to serve the site locally. You can use any server you want.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;These dependencies are used for various file transformations such as markdown to HTML, HTML formatting, and file watching.&lt;/p&gt;
&lt;h2&gt;Step 4&lt;/h2&gt;
&lt;p&gt;Create a &lt;code&gt;src&lt;/code&gt; folder and add 4 files:&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;src/build.ts&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import * as fs from &amp;quot;fs&amp;quot;;
import { compileDir, compileTarget } from &amp;quot;./compile.js&amp;quot;;
import * as path from &amp;quot;path&amp;quot;;
import chokidar from &amp;quot;chokidar&amp;quot;;
import { publicDirectory } from &amp;quot;./static.js&amp;quot;;

interface Options {
    inputDir?: string;
    outputDir?: string;
    publicDir?: string;
    watch?: boolean;
    clean?: boolean;
}

export default async function build(options: Options) {
    const inputDir = options.inputDir || &amp;quot;www&amp;quot;;
    const outputDir = options.outputDir || &amp;quot;build&amp;quot;;
    const publicDir = options.publicDir || &amp;quot;public&amp;quot;;
    const watch = options.watch || false;
    const clean = options.clean || false;

    if (!fs.existsSync(inputDir)) {
        throw new Error(`Input directory ${inputDir} does not exist`);
    }

    if (clean) {
        if (fs.existsSync(outputDir)) {
            fs.rmdirSync(outputDir, { recursive: true });
        }
    }

    if (!fs.existsSync(outputDir)) {
        fs.mkdirSync(outputDir, { recursive: true });
    }

    if (watch) {
        console.log(&amp;quot;Watching for changes...&amp;quot;);
        chokidar.watch(inputDir).on(&amp;quot;all&amp;quot;, async (event, inputFile) =&amp;gt; {
            console.log(event, inputFile);
            if (fs.existsSync(inputFile)) {
                const relativePath = path.relative(inputDir, inputFile);
                const outputFile = `${outputDir}/${relativePath}`;

                const stat = fs.statSync(inputFile);
                if (stat.isDirectory()) {
                    await compileDir(inputFile, outputFile);
                } else if (stat.isFile()) {
                    const filename = path.basename(inputFile);
                    if (filename === &amp;quot;layout.html&amp;quot;) {
                        // Rebuild all related directories
                        const dir = path.dirname(inputFile);
                        const inDir = path.relative(inputDir, dir);
                        const outDir = `${outputDir}/${inDir}`;
                        await compileDir(dir, outDir);
                    } else {
                        await compileTarget(inputFile, outputFile);
                    }
                }
            }
        });
    } else {
        await compileDir(inputDir, outputDir);
    }

    if (publicDir.split(&apos;,&apos;).length &amp;gt; 1) {
        for (const dir of publicDir.split(&apos;,&apos;)) {
            publicDirectory(dir, outputDir, watch);
        }
    } else {
        publicDirectory(publicDir, outputDir, watch);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;src/compile.ts&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import * as fs from &amp;quot;fs&amp;quot;;
import * as path from &amp;quot;path&amp;quot;;
import MarkdownIt from &amp;quot;markdown-it&amp;quot;;
import hljs from &amp;quot;highlight.js&amp;quot;;
import * as parse5 from &amp;quot;parse5&amp;quot;;
import type { Document } from &amp;quot;parse5/dist/tree-adapters/default.js&amp;quot;;
import format from &apos;html-format&apos;;

function compile(file: string) {
    const raw = fs.readFileSync(file, &amp;quot;utf-8&amp;quot;);
    const ext = path.extname(file);

    switch (ext) {
        case &amp;quot;.md&amp;quot;:
        case &amp;quot;.markdown&amp;quot;:
            const md = new MarkdownIt({
                html: true,
                linkify: true,
                typographer: true,
                highlight: function (str, lang) {
                    if (lang &amp;amp;&amp;amp; hljs.getLanguage(lang)) {
                        try {
                            return (
                                &apos;&amp;lt;pre class=&amp;quot;hljs&amp;quot;&amp;gt;&amp;lt;code&amp;gt;&apos; +
                                hljs.highlight(str, { language: lang, ignoreIllegals: true })
                                    .value +
                                &amp;quot;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;quot;
                            );
                        } catch (__) {
                            console.error(__);
                        }
                    }
                    return &amp;quot;&amp;quot;;
                },
            });
            return parse5.parse(md.render(raw));
        case &amp;quot;.html&amp;quot;:
            return parse5.parse(raw);
        default:
            break;
    }

    return raw;
}

function createHtml(options?: { head?: string; body?: string; }) {
    return `
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
&amp;lt;head&amp;gt;
${options?.head ?? &amp;quot;&amp;quot;}
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
${options?.body ?? &amp;quot;&amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;&amp;quot;}
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
`;
}

export async function compileFile(file: string, target: string) {
    // Use regex to check if ends with index.*
    const isIndex = /index\.([a-z]+)$/i.test(file);

    if (!isIndex &amp;amp;&amp;amp; !fs.statSync(file).isDirectory()) {
        // Skipping for nested layouts
        return;
    }

    const parent = path.dirname(target);

    // Render up the directory tree until we hit the root directory
    const files: string[] = [file];
    let filePath = file;
    while (filePath !== parent) {
        filePath = path.dirname(filePath);
        // Check for root level layout
        const layout = path.join(filePath, &amp;quot;layout.html&amp;quot;);
        if (fs.existsSync(layout)) {
            files.unshift(layout);
        }
        if (filePath === &amp;quot;.&amp;quot;) break;
    }

    // Check for root level index markdown or html
    const layout = path.join(parent, &amp;quot;layout.html&amp;quot;);
    if (fs.existsSync(layout)) {
        if (
            fs.existsSync(path.join(parent, &amp;quot;index.html&amp;quot;)) ||
            fs.existsSync(path.join(parent, &amp;quot;index.md&amp;quot;)) ||
            fs.existsSync(path.join(parent, &amp;quot;index.markdown&amp;quot;))
        ) {
            files.unshift(layout);
        }
    }

    let output = createHtml();

    for (const item of files) {
        const doc = compile(item);
        if (typeof doc !== &apos;string&apos;) {
            const content = parse5.serialize(doc);
            output = mergeDocuments(output, content);
        }
    }

    // Replace extension
    const ext = path.extname(file);
    const newFile = target.replace(ext, &apos;.html&apos;);

    // Check if parent directory exists
    const parentDir = path.dirname(newFile);
    if (!fs.existsSync(parentDir)) {
        fs.mkdirSync(parentDir, { recursive: true });
    }

    fs.writeFileSync(newFile, output);
    console.log(`--&amp;gt; ${newFile}`);
}

function mergeDocuments(current: string, source: string) {
    let raw = current;

    // Merge body
    const html = extractDoc(parse5.parse(source));
    // Check for &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
    const hasSlot = raw.includes(&amp;quot;&amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;&amp;quot;);
    if (hasSlot) {
        raw = raw.replace(&amp;quot;&amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;&amp;quot;, parse5.serialize(html.body));
    } else {
        // Append to body
        const endBodyIdx = raw.lastIndexOf(&amp;quot;&amp;lt;/body&amp;gt;&amp;quot;);
        const start = raw.slice(0, endBodyIdx);
        const end = raw.slice(endBodyIdx);
        const body = parse5.serialize(html.body);
        raw = start + body + end;
    }

    // Merge head
    const endHeadIdx = raw.lastIndexOf(&amp;quot;&amp;lt;/head&amp;gt;&amp;quot;);
    const start = raw.slice(0, endHeadIdx);
    const end = raw.slice(endHeadIdx);
    const head = parse5.serialize(html.head);
    raw = start + head + end;

    // Format
    raw = format(raw);

    // Remove duplicate title tags
    const lastTitle = raw.lastIndexOf(&amp;quot;&amp;lt;title&amp;gt;&amp;quot;);
    const lastTitleEnd = raw.lastIndexOf(&amp;quot;&amp;lt;/title&amp;gt;&amp;quot;);
    const title = raw.slice(lastTitle, lastTitleEnd + 8);
    raw = raw.replace(/&amp;lt;title&amp;gt;.*&amp;lt;\/title&amp;gt;/, &amp;quot;&amp;quot;);
    raw = raw.replace(&amp;quot;&amp;lt;/head&amp;gt;&amp;quot;, title + &amp;quot;&amp;lt;/head&amp;gt;&amp;quot;);

    return raw;
}

function extractDoc(doc: Document) {
    const html = (doc.childNodes[1] ?? doc.childNodes[0]) as unknown as Document;
    const head = html.childNodes.find(
        (node) =&amp;gt; node.nodeName === &amp;quot;head&amp;quot;
    ) as unknown as Document;
    const body = html.childNodes.find(
        (node) =&amp;gt; node.nodeName === &amp;quot;body&amp;quot;
    ) as unknown as Document;
    return { head, body };
}

export async function compileDir(inputDir: string, outputDir: string) {
    const files = fs.readdirSync(inputDir);
    for (const file of files) {
        const inputFile = `${inputDir}/${file}`;
        const outputFile = `${outputDir}/${file}`;

        await compileTarget(inputFile, outputFile);
    }
}

export async function compileTarget(input: string, output: string) {
    const stat = fs.statSync(input);
    if (stat.isDirectory()) {
        if (!fs.existsSync(output)) {
            fs.mkdirSync(output, { recursive: true });
        }
        await compileDir(input, output);
    } else if (stat.isFile()) {
        const ext = path.extname(input);
        if ([&apos;.html&apos;, &apos;.md&apos;, &apos;.markdown&apos;].includes(ext)) {
            await compileFile(input, output);
        } else {
            const current = fs.readFileSync(input);
            const parentDir = path.dirname(output);
            if (!fs.existsSync(parentDir)) {
                fs.mkdirSync(parentDir, { recursive: true });
            }
            if (fs.existsSync(output)) {
                // Check if content is the same
                const previous = fs.readFileSync(output);
                if (Buffer.compare(current, previous) !== 0) {
                    fs.writeFileSync(output, current);
                }
            } else {
                // Copy the file
                fs.copyFileSync(input, output);
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;src/static.ts&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import * as fs from &amp;quot;fs&amp;quot;;
import * as path from &amp;quot;path&amp;quot;;
import chokidar from &amp;quot;chokidar&amp;quot;;

export function publicDirectory(publicDir: string, outputDir: string, watch: boolean) {
    if (watch) {
        if (fs.existsSync(publicDir)) {
            chokidar.watch(publicDir).on(&amp;quot;all&amp;quot;, (event, inputFile) =&amp;gt; {
                console.log(event, inputFile);
                if (fs.existsSync(inputFile)) {
                    const relativePath = path.relative(publicDir, inputFile);
                    const outputFile = `${outputDir}/${relativePath}`;

                    const stat = fs.statSync(inputFile);
                    if (stat.isDirectory()) {
                        copyStaticFiles(inputFile, outputFile);
                    } else if (stat.isFile()) {
                        fs.copyFileSync(inputFile, outputFile);
                    }
                }
            });
        }
    } else {
        // Copy static files
        if (fs.existsSync(publicDir)) {
            copyStaticFiles(publicDir, outputDir);
        }
    }
}

function copyStaticFiles(inDir: string, outDir: string) {
    const files = fs.readdirSync(inDir);
    for (const file of files) {
        const inputFile = `${inDir}/${file}`;
        const outputFile = `${outDir}/${file}`;

        const stat = fs.statSync(inputFile);
        if (stat.isDirectory()) {
            if (!fs.existsSync(outputFile)) {
                fs.mkdirSync(outputFile, { recursive: true });
            }

            copyStaticFiles(inputFile, outputFile);
        } else if (stat.isFile()) {
            fs.copyFileSync(inputFile, outputFile);
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;src/main.ts&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;#!/usr/bin/env node

// @ts-ignore
import { parseArgs } from &amp;quot;@pkgjs/parseargs&amp;quot;;
import build from &amp;quot;./build.js&amp;quot;;

export async function main() {
  const {
    values: { inputDir, outputDir, watch },
  } = parseArgs({
    options: {
      inputDir: {
        type: &amp;quot;string&amp;quot;,
        short: &amp;quot;i&amp;quot;,
      },
      outputDir: {
        type: &amp;quot;string&amp;quot;,
        short: &amp;quot;o&amp;quot;,
      },
      watch: {
        type: &amp;quot;boolean&amp;quot;,
        short: &amp;quot;w&amp;quot;,
      },
    },
    allowPositional: true,
  });

  if (inputDir === undefined || outputDir === undefined) {
    console.log(&amp;quot;Usage: build -i &amp;lt;inputDir&amp;gt; -o &amp;lt;outputDir&amp;gt; [-w]&amp;quot;);
    return;
  }

  await build({
    inputDir,
    outputDir,
    watch,
  });
}

main();

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 5&lt;/h2&gt;
&lt;p&gt;Now that the project is setup we can start the typescript compiler in watch mode.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm run ts:watch
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 6&lt;/h2&gt;
&lt;p&gt;Now create a folder that will contain the source files for the website.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;example/index.md&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# Hello World

This is a test
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;example/style.css&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;body {
    background-color: #000;
    color: #fff;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;example/layout.html&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;
    &amp;lt;meta http-equiv=&amp;quot;X-UA-Compatible&amp;quot; content=&amp;quot;IE=edge&amp;quot;&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;
    &amp;lt;title&amp;gt;Example&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;style.css&amp;quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 7&lt;/h2&gt;
&lt;p&gt;With the source files created we can now run the build script.&lt;/p&gt;
&lt;p&gt;For a one time build run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a watch mode run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now start the http server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm run start
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As you make changes it will only update affected files and be very fast to update.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that this does not bundle the javascript and will be up to you if you are using &lt;code&gt;node_modules&lt;/code&gt; in any files (for the example in the repo I show how to use &lt;strong&gt;UNPKG&lt;/strong&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you want to find the source code you can check it out &lt;a href=&quot;https://github.com/rodydavis/static-site-file-based-routing&quot;&gt;here&lt;/a&gt; otherwise thanks for reading and let me know if you have any questions!&lt;/p&gt;
</content:encoded></item><item><title>Building a HTML Element Sandbox with Lit</title><link>https://rodydavis.com/web/html-code-sandbox/</link><guid isPermaLink="true">https://rodydavis.com/web/html-code-sandbox/</guid><description>Create a reusable HTML element sandbox using Lit, TypeScript, and Vite to dynamically update web components.</description><pubDate>Sun, 19 Jan 2025 06:17:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building a HTML Element Sandbox with Lit&lt;/h1&gt;
&lt;p&gt;In this article I will go over how to set up a &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; web component and use it to create a HTML Element sandbox that can be used to update a live component.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; The final source &lt;a href=&quot;https://github.com/rodydavis/html-element-sandbox&quot;&gt;here&lt;/a&gt; and an online &lt;a href=&quot;https://rodydavis.github.io/html-element-sandbox/&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vscode&lt;/li&gt;
&lt;li&gt;Node &amp;gt;= 16&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;We can start off by navigating in terminal to the location of the project and run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;npm init @vitejs/app --template lit-ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enter a project name &lt;code&gt;html-element-sandbox&lt;/code&gt; and now open the project in vscode and install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;cd html-element-sandbox
npm i lit
npm i -D @types/node
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;vite.config.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from &amp;quot;vite&amp;quot;;
import { resolve } from &amp;quot;path&amp;quot;;

export default defineConfig({
  base: &amp;quot;/html-element-sandbox/&amp;quot;,
  build: {
    lib: {
      entry: &amp;quot;src/html-element-sandbox.ts&amp;quot;,
      formats: [&amp;quot;es&amp;quot;],
    },
    rollupOptions: {
      input: {
        main: resolve(__dirname, &amp;quot;index.html&amp;quot;),
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Template&lt;/h2&gt;
&lt;p&gt;Open up the &lt;code&gt;index.html&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/src/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;HTML Element Sandbox&amp;lt;/title&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/html-element-sandbox.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        margin: 0;
        padding: 0;
        font-family: sans-serif;
      }
      html-element-sandbox {
        display: block;
        width: 100%;
        height: 100vh;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;html-element-sandbox&amp;gt;
      &amp;lt;template&amp;gt;
        &amp;lt;button
          class=&amp;quot;button&amp;quot;
          knob-text=&amp;quot;label&amp;quot;
          knob-css-color=&amp;quot;fg-color&amp;quot;
          knob-css-background-color=&amp;quot;bg-color&amp;quot;
          knob-css-border-radius=&amp;quot;shape&amp;quot;
          knob-css-font-size=&amp;quot;text-font-size&amp;quot;
          knob-css-padding=&amp;quot;padding&amp;quot;
          knob-css---shadow-color=&amp;quot;shadow&amp;quot;
        &amp;gt;
          My Button
        &amp;lt;/button&amp;gt;
        &amp;lt;style&amp;gt;
          .button {
            --shadow-color: #000;
            --elevation: 3px;
            display: block;
            width: 100%;
            height: 100%;
            border: none;
            background-color: transparent;
            cursor: pointer;
            box-shadow: 0 var(--elevation) calc(var(--elevation) * 2) 0 var(--shadow-color);
          }
        &amp;lt;/style&amp;gt;
      &amp;lt;/template&amp;gt;
      &amp;lt;div slot=&amp;quot;knobs&amp;quot;&amp;gt;
        &amp;lt;knob-string id=&amp;quot;label&amp;quot; name=&amp;quot;Label&amp;quot; value=&amp;quot;BUTTON&amp;quot;&amp;gt;&amp;lt;/knob-string&amp;gt;
        &amp;lt;knob-group name=&amp;quot;Style&amp;quot; expanded&amp;gt;
          &amp;lt;knob-color
            id=&amp;quot;bg-color&amp;quot;
            name=&amp;quot;Background Color&amp;quot;
            value=&amp;quot;#ff0000&amp;quot;
          &amp;gt;&amp;lt;/knob-color&amp;gt;
          &amp;lt;knob-color
            id=&amp;quot;fg-color&amp;quot;
            name=&amp;quot;Foreground Color&amp;quot;
            value=&amp;quot;#ffffff&amp;quot;
          &amp;gt;&amp;lt;/knob-color&amp;gt;
          &amp;lt;knob-color
            id=&amp;quot;shadow&amp;quot;
            name=&amp;quot;Shadow Color&amp;quot;
            value=&amp;quot;#000000&amp;quot;
          &amp;gt;&amp;lt;/knob-color&amp;gt;
          &amp;lt;knob-number
            id=&amp;quot;text-font-size&amp;quot;
            name=&amp;quot;Font Size&amp;quot;
            value=&amp;quot;16&amp;quot;
            suffix=&amp;quot;px&amp;quot;
          &amp;gt;&amp;lt;/knob-number&amp;gt;
          &amp;lt;knob-number
            id=&amp;quot;shape&amp;quot;
            name=&amp;quot;Border Radius&amp;quot;
            value=&amp;quot;100&amp;quot;
            suffix=&amp;quot;px&amp;quot;
          &amp;gt;&amp;lt;/knob-number&amp;gt;
          &amp;lt;knob-number
            id=&amp;quot;padding&amp;quot;
            name=&amp;quot;Padding&amp;quot;
            value=&amp;quot;12&amp;quot;
            suffix=&amp;quot;px&amp;quot;
          &amp;gt;&amp;lt;/knob-number&amp;gt;
        &amp;lt;/knob-group&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/html-element-sandbox&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are defining the markup we want to use in our sandbox. We are using the &lt;code&gt;html-element-sandbox&lt;/code&gt; component to create a sandbox for our HTML Element.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;html-element-sandbox&amp;gt;&amp;lt;/html-element-sandbox&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each knob is defined by an &lt;code&gt;id&lt;/code&gt; and a &lt;code&gt;name&lt;/code&gt;. The &lt;code&gt;id&lt;/code&gt; is used to identify the knob in the &lt;code&gt;template&lt;/code&gt; and the &lt;code&gt;name&lt;/code&gt; is used to display the knob in the UI.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;knob-number
  id=&amp;quot;shape&amp;quot;
  name=&amp;quot;Border Radius&amp;quot;
  value=&amp;quot;30&amp;quot;
  suffix=&amp;quot;px&amp;quot;
&amp;gt;&amp;lt;/knob-number&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the element inside the &lt;code&gt;template&lt;/code&gt; we use &lt;code&gt;knob-*&lt;/code&gt; attributes to get the values of the knobs and set the attributes, CSS style or text content.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;!-- Attributes --&amp;gt;
&amp;lt;div knob-attr-disabled=&amp;quot;disabled&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;knob-boolean id=&amp;quot;disabled&amp;quot; name=&amp;quot;Disable&amp;quot; value=&amp;quot;false&amp;quot;&amp;gt;&amp;lt;/knob-boolean&amp;gt;

&amp;lt;!-- CSS Properties --&amp;gt;
&amp;lt;div knob-css-color=&amp;quot;fg-color&amp;quot; knob-css-background-color=&amp;quot;bg-color&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;knob-color id=&amp;quot;bg-color&amp;quot; name=&amp;quot;Background Color&amp;quot; value=&amp;quot;#ff0000&amp;quot;&amp;gt;&amp;lt;/knob-color&amp;gt;
&amp;lt;knob-color id=&amp;quot;fg-color&amp;quot; name=&amp;quot;Foreground Color&amp;quot; value=&amp;quot;#ffffff&amp;quot;&amp;gt;&amp;lt;/knob-color&amp;gt;

&amp;lt;!-- Text Content --&amp;gt;
&amp;lt;div knob-text=&amp;quot;content&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;knob-string id=&amp;quot;content&amp;quot; name=&amp;quot;Text Content&amp;quot; value=&amp;quot;Hello World&amp;quot;&amp;gt;&amp;lt;/knob-string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A single knob can point to multiple elements:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;&amp;lt;html-element-sandbox&amp;gt;
  &amp;lt;template&amp;gt;
    &amp;lt;div id=&amp;quot;buttons&amp;quot;&amp;gt;
      &amp;lt;button
        knob-text=&amp;quot;label&amp;quot;
        knob-css-color=&amp;quot;fg-color&amp;quot;
        knob-css-background-color=&amp;quot;bg-color&amp;quot;
        knob-css-border-radius=&amp;quot;shape&amp;quot;
        knob-css-font-size=&amp;quot;text-font-size&amp;quot;
        knob-css-padding=&amp;quot;padding&amp;quot;
        knob-css---shadow-color=&amp;quot;shadow&amp;quot;
        knob-attr-raised=&amp;quot;raised&amp;quot;
        knob-attr-contenteditable=&amp;quot;contenteditable&amp;quot;
      &amp;gt;&amp;lt;/button&amp;gt;
      &amp;lt;mwc-button
        knob-attr-label=&amp;quot;label&amp;quot;
        knob-css---mdc-theme-on-primary=&amp;quot;fg-color&amp;quot;
        knob-css---mdc-theme-primary=&amp;quot;bg-color&amp;quot;
        knob-css---mdc-shape-small=&amp;quot;shape&amp;quot;
        knob-attr-raised=&amp;quot;raised&amp;quot;
        label=&amp;quot;My Button&amp;quot;
      &amp;gt;&amp;lt;/mwc-button&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot;&amp;gt;
      import &amp;quot;https://www.unpkg.com/@material/mwc-button@0.26.1/mwc-button.js?module&amp;quot;;
    &amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
      button {
        --shadow-color: #000;
        --elevation: 3px;
        display: block;
        border: none;
        background-color: transparent;
        cursor: pointer;
        box-shadow: 0 var(--elevation) calc(var(--elevation) * 2) 0 var(--shadow-color);
      }
      mwc-button {
        --mdc-theme-on-primary: #000;
        --mdc-theme-primary: #fff;
        --mdc-shape-small: none;
      }
      #buttons {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        gap: 1rem;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/template&amp;gt;
  &amp;lt;div slot=&amp;quot;knobs&amp;quot;&amp;gt;
    &amp;lt;knob-string id=&amp;quot;label&amp;quot; name=&amp;quot;Label&amp;quot; value=&amp;quot;BUTTON&amp;quot;&amp;gt;&amp;lt;/knob-string&amp;gt;
    &amp;lt;knob-group name=&amp;quot;Style&amp;quot; expanded&amp;gt;
      &amp;lt;knob-color
        id=&amp;quot;bg-color&amp;quot;
        name=&amp;quot;Background Color&amp;quot;
        value=&amp;quot;#ff0000&amp;quot;
      &amp;gt;&amp;lt;/knob-color&amp;gt;
      &amp;lt;knob-color
        id=&amp;quot;fg-color&amp;quot;
        name=&amp;quot;Foreground Color&amp;quot;
        value=&amp;quot;#ffffff&amp;quot;
      &amp;gt;&amp;lt;/knob-color&amp;gt;
      &amp;lt;knob-color id=&amp;quot;shadow&amp;quot; name=&amp;quot;Shadow Color&amp;quot; value=&amp;quot;#000000&amp;quot;&amp;gt;&amp;lt;/knob-color&amp;gt;
      &amp;lt;knob-number
        id=&amp;quot;text-font-size&amp;quot;
        name=&amp;quot;Font Size&amp;quot;
        value=&amp;quot;16&amp;quot;
        suffix=&amp;quot;px&amp;quot;
      &amp;gt;&amp;lt;/knob-number&amp;gt;
      &amp;lt;knob-number
        id=&amp;quot;shape&amp;quot;
        name=&amp;quot;Border Radius&amp;quot;
        value=&amp;quot;30&amp;quot;
        suffix=&amp;quot;px&amp;quot;
      &amp;gt;&amp;lt;/knob-number&amp;gt;
      &amp;lt;knob-number
        id=&amp;quot;padding&amp;quot;
        name=&amp;quot;Padding&amp;quot;
        value=&amp;quot;12&amp;quot;
        suffix=&amp;quot;px&amp;quot;
      &amp;gt;&amp;lt;/knob-number&amp;gt;
    &amp;lt;/knob-group&amp;gt;
    &amp;lt;knob-group name=&amp;quot;Attributes&amp;quot; expanded&amp;gt;
      &amp;lt;knob-boolean id=&amp;quot;raised&amp;quot; name=&amp;quot;Raised&amp;quot; value=&amp;quot;false&amp;quot;&amp;gt;&amp;lt;/knob-boolean&amp;gt;
      &amp;lt;knob-list id=&amp;quot;contenteditable&amp;quot; name=&amp;quot;Content Editable&amp;quot; value=&amp;quot;false&amp;quot;&amp;gt;
        &amp;lt;option value=&amp;quot;true&amp;quot;&amp;gt;true&amp;lt;/option&amp;gt;
        &amp;lt;option value=&amp;quot;false&amp;quot;&amp;gt;false&amp;lt;/option&amp;gt;
      &amp;lt;/knob-list&amp;gt;
    &amp;lt;/knob-group&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/html-element-sandbox&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;style&lt;/code&gt; and &lt;code&gt;script&lt;/code&gt; can be added to load extra content into the sandbox (e.g. a &lt;code&gt;script&lt;/code&gt; to load a web component).&lt;/p&gt;
&lt;h2&gt;Web Component&lt;/h2&gt;
&lt;p&gt;Before we update our component we need to rename &lt;code&gt;my-element.ts&lt;/code&gt; to &lt;code&gt;html-element-sandbox.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open up &lt;code&gt;html-element-sandbox.ts&lt;/code&gt; and update it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { css, html, LitElement } from &amp;quot;lit&amp;quot;;
import { customElement, state } from &amp;quot;lit/decorators.js&amp;quot;;

import &amp;quot;./knobs/boolean&amp;quot;;
import &amp;quot;./knobs/string&amp;quot;;
import &amp;quot;./knobs/number&amp;quot;;
import &amp;quot;./knobs/color&amp;quot;;
import &amp;quot;./knobs/list&amp;quot;;
import &amp;quot;./knobs/group&amp;quot;;
import { KnobValue } from &amp;quot;./knobs/base&amp;quot;;
import { BooleanKnob } from &amp;quot;./knobs/boolean&amp;quot;;

export const tagName = &amp;quot;html-element-sandbox&amp;quot;;

@customElement(tagName)
export class HTMLElementSandbox extends LitElement {
  static styles = css`
    main {
      --knobs-width: 300px;
      --code-height: calc(100% * 0.4);
      --mobile-height: 350px;
      display: grid;
      grid-template-areas: &amp;quot;preview&amp;quot; &amp;quot;knobs&amp;quot; &amp;quot;code&amp;quot;;
      grid-template-columns: 100%;
      grid-template-rows: var(--mobile-height) auto auto;
      height: 100%;
      width: 100%;
    }
    #preview {
      grid-area: preview;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      border-bottom: 1px solid #272727;
      background-color: whitesmoke;
    }
    @media (min-width: 600px) {
      main {
        grid-template-areas:
          &amp;quot;preview knobs&amp;quot;
          &amp;quot;code knobs&amp;quot;;
        grid-template-columns: calc(100% - var(--knobs-width)) var(
            --knobs-width
          );
        grid-template-rows: calc(100% - var(--code-height)) var(--code-height);
      }
      #preview {
        border-bottom: none;
      }
      slot[name=&amp;quot;knobs&amp;quot;] {
        overflow-y: auto;
      }
      pre {
        overflow-y: scroll;
      }
    }
    section {
      flex: 1;
    }
    slot[name=&amp;quot;knobs&amp;quot;] {
      grid-area: knobs;
      display: flex;
      flex-direction: column;
      border-left: 1px solid #000;
    }
    slot[name=&amp;quot;code&amp;quot;] {
      grid-area: code;
    }
    pre {
      margin: 0;
      font-family: Monaco, Courier, monospace;
      padding: 16px;
      background-color: #272727;
      color: #c8c8c8;
    }
    code {
      font-size: 0.8rem;
      white-space: pre-wrap;
    }
  `;

  @state() code = &amp;quot;&amp;quot;;

  render() {
    return html`&amp;lt;main&amp;gt;
      &amp;lt;section id=&amp;quot;preview&amp;quot;&amp;gt;
        &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
      &amp;lt;/section&amp;gt;
      &amp;lt;slot name=&amp;quot;knobs&amp;quot;&amp;gt; &amp;lt;/slot&amp;gt;
      &amp;lt;slot name=&amp;quot;code&amp;quot;&amp;gt;
        &amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;${this.code}&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;
      &amp;lt;/slot&amp;gt;
    &amp;lt;/main&amp;gt;`;
  }

  firstUpdated() {
    this.init();
  }

  init() {
    this.setUpKnobs();
    this.code = this.getCode();
    // Update the code every time a knob value changes
    this.addEventListener(&amp;quot;value&amp;quot;, () =&amp;gt; {
      this.code = this.getCode();
    });
  }

  setUpKnobs() {
    const root = this.shadowRoot!;
    const preview = root.getElementById(&amp;quot;preview&amp;quot;)!;
    const template = this.querySelector(&amp;quot;template&amp;quot;);
    if (template) {
      const div = document.createElement(&amp;quot;div&amp;quot;);
      div.appendChild(template.content.cloneNode(true));
      // Text Knobs (knob-text)
      div.querySelectorAll(&amp;quot;[knob-text]&amp;quot;).forEach((el) =&amp;gt; {
        const elemId = el.getAttribute(&amp;quot;knob-text&amp;quot;) || &amp;quot;&amp;quot;;
        const knob = this.querySelector(`#${elemId}`);
        if (knob &amp;amp;&amp;amp; knob instanceof KnobValue) {
          knob.addEventListener(&amp;quot;value&amp;quot;, () =&amp;gt; {
            const val = knob.value;
            el.textContent = val;
          });
          el.addEventListener(&amp;quot;input&amp;quot;, (e) =&amp;gt; {
            const target = e.target as HTMLElement;
            knob.value = target.textContent;
          });
          knob.init();
        }
      });
      div.querySelectorAll(&amp;quot;*&amp;quot;).forEach((el) =&amp;gt; {
        const attrs = el.attributes;
        for (let i = 0; i &amp;lt; attrs.length; i++) {
          const attr = attrs[i];
          const attrName = attr.name;
          // CSS Knobs (knob-css-*)
          if (attrName.startsWith(&amp;quot;knob-css-&amp;quot;)) {
            const cssKey = attrName.replace(&amp;quot;knob-css-&amp;quot;, &amp;quot;&amp;quot;);
            const knob = this.querySelector(`#${attr.value}`);
            if (
              knob &amp;amp;&amp;amp;
              knob instanceof KnobValue &amp;amp;&amp;amp;
              el instanceof HTMLElement
            ) {
              knob.addEventListener(&amp;quot;value&amp;quot;, () =&amp;gt; {
                const val = knob.value;
                if (knob.hasAttribute(&amp;quot;suffix&amp;quot;)) {
                  // Add suffix to the value (e.g. px)
                  el.style.setProperty(
                    cssKey,
                    val + knob.getAttribute(&amp;quot;suffix&amp;quot;)
                  );
                } else {
                  // No suffix, just set the value
                  el.style.setProperty(cssKey, val);
                }
              });
              knob.init();
            }
          }
          // Attribute Knobs (knob-attr-*)
          if (attrName.startsWith(&amp;quot;knob-attr-&amp;quot;)) {
            const attrKey = attrName.replace(&amp;quot;knob-attr-&amp;quot;, &amp;quot;&amp;quot;);
            const knob = this.querySelector(`#${attr.value}`);
            if (knob &amp;amp;&amp;amp; knob instanceof KnobValue) {
              knob.addEventListener(&amp;quot;value&amp;quot;, () =&amp;gt; {
                const val = knob.value;
                if (knob instanceof BooleanKnob) {
                  if (val) {
                    // &amp;lt;div hidden&amp;gt;
                    el.setAttribute(attrKey, &amp;quot;&amp;quot;);
                  } else {
                    // &amp;lt;div&amp;gt;
                    el.removeAttribute(attrKey);
                  }
                } else {
                  // &amp;lt;div value=&amp;quot;foo&amp;quot;&amp;gt;
                  el.setAttribute(attrKey, val);
                }
              });
              knob.init();
            }
          }
        }
      });
      preview.appendChild(div);
    }
  }

  getCode() {
    const root = this.shadowRoot!;
    const preview = root.getElementById(&amp;quot;preview&amp;quot;)!;
    if (preview.children.length &amp;gt; 0) {
      const child = preview.children[1];
      if (child &amp;amp;&amp;amp; child.children.length &amp;gt; 0) {
        const lines = this.elementToString(child.children[0]);
        // Trim empty lines
        const linesArray = lines.split(&amp;quot;\n&amp;quot;);
        const filteredLines = linesArray.filter((line) =&amp;gt; line.trim() !== &amp;quot;&amp;quot;);
        return filteredLines.join(&amp;quot;\n&amp;quot;);
      }
    }
    return &amp;quot;&amp;quot;;
  }

  elementToString(node: Element) {
    const sb: string[] = [];
    const tag = node.tagName.toLowerCase();
    sb.push(`&amp;lt;${tag}`);
    const attrs = node.attributes;
    // Add attributes
    for (let i = 0; i &amp;lt; attrs.length; i++) {
      const attr = attrs[i];
      if (attr.name.startsWith(&amp;quot;knob-&amp;quot;)) continue;
      // If the attribute is a boolean attribute, add it only if it&apos;s true
      if (attr.value === &amp;quot;&amp;quot;) {
        sb.push(` ${attr.name}`);
      } else {
        sb.push(` ${attr.name}=&amp;quot;${attr.value}&amp;quot;`);
      }
    }
    sb.push(&amp;quot;&amp;gt;&amp;quot;);
    if (node.childNodes.length &amp;gt; 0) {
      for (let i = 0; i &amp;lt; node.childNodes.length; i++) {
        const child = node.childNodes[i];
        // If the child is a text node, add the content
        if (child instanceof Text) {
          sb.push(child.textContent || &amp;quot;&amp;quot;);
        } else if (child instanceof Element) {
          // If the child is an element, recurse
          sb.push(this.elementToString(child));
        }
      }
    }
    sb.push(`&amp;lt;/${tag}&amp;gt;`);
    return sb.join(&amp;quot;\n&amp;quot;);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [tagName]: HTMLElementSandbox;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Knobs&lt;/h2&gt;
&lt;p&gt;First let up create a base class that will be used to create all other knobs. Create &lt;code&gt;src/knobs/base.ts&lt;/code&gt; and update with with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { css, html, LitElement, TemplateResult } from &amp;quot;lit&amp;quot;;
import { property } from &amp;quot;lit/decorators.js&amp;quot;;

export class Knob extends LitElement {
  constructor(name: string) {
    super();
    this.name = name;
  }

  @property() name: string;
}

export abstract class KnobValue&amp;lt;T&amp;gt; extends Knob {
  constructor(name: string, public val: T) {
    super(name);
    this._value = val;
    this.notify();
  }

  static styles = css`
    .knob {
      display: flex;
      flex-direction: row;
      align-items: center;
      padding: 0.5rem;
    }
    .knob label {
      flex: 1;
    }
  `;

  _value: T;

  get value(): T {
    return this._value;
  }

  set value(value: T) {
    this._value = value;
    this.notify();
  }

  notify() {
    const value = this.value;
    this.onValue(value);
    this.dispatchEvent(
      new CustomEvent(&amp;quot;value&amp;quot;, {
        detail: value,
        bubbles: true,
        composed: true,
      })
    );
    this.requestUpdate();
  }

  render() {
    return html`
      &amp;lt;div class=&amp;quot;knob&amp;quot;&amp;gt;
        &amp;lt;label&amp;gt;${this.name}&amp;lt;/label&amp;gt;
        ${this.buildInput()}
      &amp;lt;/div&amp;gt;
    `;
  }

  onValue(_val: T) {}

  init() {
    this.notify();
  }

  resolveValue(val: T) {
    return val;
  }

  abstract buildInput(): TemplateResult;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Boolean knob&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;src/knobs/boolean.ts&lt;/code&gt; and update with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { KnobValue } from &amp;quot;./base&amp;quot;;

import { html } from &amp;quot;lit&amp;quot;;
import { customElement, property } from &amp;quot;lit/decorators.js&amp;quot;;

export const tagName = &amp;quot;knob-boolean&amp;quot;;

@customElement(tagName)
export class BooleanKnob extends KnobValue&amp;lt;boolean&amp;gt; {
  constructor(name: string, val: boolean) {
    super(name, val);
  }

  static styles = KnobValue.styles;

  @property({
    type: Boolean,
    attribute: &amp;quot;value&amp;quot;,
  })
  _value = false;

  buildInput() {
    return html`&amp;lt;input
      type=&amp;quot;checkbox&amp;quot;
      .checked=${this.resolveValue(this.value)}
      @change=${this.onChange}
    /&amp;gt;`;
  }

  onChange(e: Event) {
    const target = e.target as HTMLInputElement;
    this.value = target.checked;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [tagName]: BooleanKnob;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Number Knob&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;src/knobs/number.ts&lt;/code&gt; and update with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { KnobValue } from &amp;quot;./base&amp;quot;;

import { html } from &amp;quot;lit&amp;quot;;
import { customElement, property } from &amp;quot;lit/decorators.js&amp;quot;;

export const tagName = &amp;quot;knob-number&amp;quot;;

@customElement(tagName)
export class NumberKnob extends KnobValue&amp;lt;number&amp;gt; {
  constructor(name: string, val: number) {
    super(name, val);
  }

  static styles = KnobValue.styles;

  @property({
    type: Number,
    attribute: &amp;quot;value&amp;quot;,
    converter: {
      fromAttribute: (val: string) =&amp;gt; parseFloat(val),
      toAttribute: (val: boolean) =&amp;gt; val.toString(),
    },
  })
  _value = 0;

  buildInput() {
    return html`&amp;lt;input
      type=&amp;quot;number&amp;quot;
      .valueAsNumber=${this.resolveValue(this.value)}
      @change=${this.onChange}
    /&amp;gt;`;
  }

  onChange(e: Event) {
    const target = e.target as HTMLInputElement;
    this.value = target.valueAsNumber;
  }

  resolveValue(val: number): number {
    return val;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [tagName]: NumberKnob;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;String Knob&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;src/knobs/string.ts&lt;/code&gt; and update with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { KnobValue } from &amp;quot;./base&amp;quot;;

import { html } from &amp;quot;lit&amp;quot;;
import { customElement, property } from &amp;quot;lit/decorators.js&amp;quot;;

export const tagName = &amp;quot;knob-string&amp;quot;;

@customElement(tagName)
export class StringKnob extends KnobValue&amp;lt;string&amp;gt; {
  constructor(name: string, val: string) {
    super(name, val);
  }

  static styles = KnobValue.styles;

  @property({ type: String, attribute: &amp;quot;value&amp;quot; })
  _value = &amp;quot;&amp;quot;;

  buildInput() {
    return html`&amp;lt;input
      type=&amp;quot;text&amp;quot;
      .value=${this.resolveValue(this.value)}
      @input=${this.onChange}
    /&amp;gt;`;
  }

  onChange(e: Event) {
    const target = e.target as HTMLInputElement;
    this.value = target.value;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [tagName]: StringKnob;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Color Knob&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;src/knobs/color.ts&lt;/code&gt; and update with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { html } from &amp;quot;lit&amp;quot;;
import { customElement } from &amp;quot;lit/decorators.js&amp;quot;;
import { StringKnob } from &amp;quot;./string&amp;quot;;

export const tagName = &amp;quot;knob-color&amp;quot;;

@customElement(tagName)
export class ColorKnob extends StringKnob {
  buildInput() {
    return html`&amp;lt;input
      type=&amp;quot;color&amp;quot;
      .value=${this.resolveValue(this.value)}
      @input=${this.onChange}
    /&amp;gt;`;
  }

  resolveValue(value: string) {
    if (value &amp;amp;&amp;amp; value.startsWith(&amp;quot;--&amp;quot;)) {
      const style = getComputedStyle(document.body);
      const resolved = style.getPropertyValue(value);
      return resolved;
    }
    return value;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [tagName]: ColorKnob;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;List Knob&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;src/knobs/list.ts&lt;/code&gt; and update with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { KnobValue } from &amp;quot;./base&amp;quot;;

import { html } from &amp;quot;lit&amp;quot;;
import { customElement, property } from &amp;quot;lit/decorators.js&amp;quot;;

export const tagName = &amp;quot;knob-list&amp;quot;;

@customElement(tagName)
export class ListKnob extends KnobValue&amp;lt;string&amp;gt; {
  constructor(name: string, val: string) {
    super(name, val);
  }

  static styles = KnobValue.styles;

  @property({
    type: String,
    attribute: &amp;quot;value&amp;quot;,
  })
  _value = &amp;quot;&amp;quot;;

  buildInput() {
    const options = this.getOptions();
    return html`&amp;lt;select @change=${this.onChange}&amp;gt;
      ${Array.from(options).map(
        (option) =&amp;gt;
          html`&amp;lt;option
            value=${option.value}
            .selected=${this.value === option.value}
          &amp;gt;
            ${option.textContent}
          &amp;lt;/option&amp;gt;`
      )}
    &amp;lt;/select&amp;gt;`;
  }

  getOptions() {
    const options = this.querySelectorAll(
      &amp;quot;option&amp;quot;
    ) as NodeListOf&amp;lt;HTMLOptionElement&amp;gt;;
    return Array.from(options);
  }

  onChange(e: Event) {
    const target = e.target as HTMLSelectElement;
    this.value = target.value;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [tagName]: ListKnob;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Group Knob&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;src/knobs/group.ts&lt;/code&gt; and update with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { css, html } from &amp;quot;lit&amp;quot;;
import { customElement, property } from &amp;quot;lit/decorators.js&amp;quot;;
import { Knob } from &amp;quot;./base&amp;quot;;

export const tagName = &amp;quot;knob-group&amp;quot;;

@customElement(tagName)
export class GroupKnob extends Knob {
  constructor(name: string, knobs: Knob[] = []) {
    super(name);
    this.knobs = knobs;
  }

  static styles = css`
    details {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
    }
    details summary {
      padding: 0.5rem;
    }
  `;

  knobs: Knob[];

  @property({ type: Boolean }) expanded = false;

  render() {
    return html`&amp;lt;details ?open=${this.expanded}&amp;gt;
      &amp;lt;summary&amp;gt;${this.name}&amp;lt;/summary&amp;gt;
      &amp;lt;div class=&amp;quot;collection&amp;quot;&amp;gt;
        &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
        ${this.knobs.map((knob) =&amp;gt; html`${knob}`)}
      &amp;lt;/div&amp;gt;
    &amp;lt;/details&amp;gt;`;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [tagName]: GroupKnob;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If everything worked as expected, you should see the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/knobs_1_dvkg6xds73.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you want to learn more about building with Lit you can read the docs &lt;a href=&quot;https://lit.dev/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The source for this example can be found &lt;a href=&quot;https://github.com/rodydavis/html-element-sandbox&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Async Preact Signals</title><link>https://rodydavis.com/web/preact-signals/</link><guid isPermaLink="true">https://rodydavis.com/web/preact-signals/</guid><description>Learn how to effectively use async data with preact/signals, including handling Promises and rerun operations for dynamic updates, while understanding the importance of synchronous computations for signal integrity.</description><pubDate>Tue, 28 Jan 2025 12:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Async Preact Signals&lt;/h1&gt;
&lt;p&gt;When working with &lt;a href=&quot;https://github.com/preactjs/signals&quot;&gt;signals&lt;/a&gt; in Javascript, it is very common to work with async data from &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise&quot;&gt;Promises&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Async vs Sync&lt;/h2&gt;
&lt;p&gt;But unlike other state management libraries, signals do not have an &lt;em&gt;asynchronous&lt;/em&gt; state graph and all values must be computed &lt;em&gt;synchronously&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;When people first start using signals they want to simply add &lt;strong&gt;async&lt;/strong&gt; to the function callback but this breaks how they work under the hood and leads to &lt;strong&gt;undefined&lt;/strong&gt; behavior. ☹️&lt;/p&gt;
&lt;p&gt;Async functions are a leaky abstraction and force you to handle them all the way up the graph. Async is also not always better and can have a &lt;a href=&quot;https://madelinemiller.dev/blog/javascript-promise-overhead/&quot;&gt;performance impact&lt;/a&gt;. 😬&lt;/p&gt;
&lt;h2&gt;Working with Promises&lt;/h2&gt;
&lt;p&gt;We can still do so much with sync operations, and make it eaiser to work with common async patterns.&lt;/p&gt;
&lt;p&gt;For example when you make a &lt;strong&gt;http&lt;/strong&gt; request using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch&quot;&gt;fetch&lt;/a&gt;, you want to return the data in the &lt;strong&gt;Promise&lt;/strong&gt; and update some UI.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const el = document.querySelector(&apos;#output&apos;);
let postId = &apos;123&apos;;
fetch(`/posts/${postId}`).then(res =&amp;gt; res.json()).then(post =&amp;gt; {
    el.innerText = post.title;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when we add signals we can rerun the fetch everytime the post id changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { effect, signal } from &amp;quot;@preact/signals-core&amp;quot;;

const el = document.querySelector(&apos;#output&apos;);
const postId =  signal( &apos;123&apos;);

effect(() =&amp;gt; {
    fetch(`/posts/${postId.value}`).then(res =&amp;gt; res.json()).then(post =&amp;gt; {
        el.innerText = post.title;
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is better, but now we need to handle stopping the previous request if the post id changes before the previous fetch completes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { effect, signal } from &amp;quot;@preact/signals-core&amp;quot;;

const el = document.querySelector(&apos;#output&apos;);
const postId =  signal( &apos;123&apos;);
let controller;

effect(() =&amp;gt; {
    if (controller) {
         controller.abort();
    }
    controller = new AbortController();
    const signal = controller.signal;
    try {
       fetch(`/posts/${postId.value}`, { signal }).then(res =&amp;gt; res.json()).then(post =&amp;gt; {
            el.innerText = post.title;
        }); 
    } catch (err) {
       // todo: show error message
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But this still skips a lot of things we normally want to show like loading states and error states.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { effect, signal, batch } from &amp;quot;@preact/signals-core&amp;quot;;

const el = document.querySelector(&apos;#output&apos;);
const postId =  signal( &apos;123&apos;);
const postData = signal({});
const errorMessage = signal(&apos;&apos;);
const loading = signal(false);
let controller;

effect(() =&amp;gt; {
    if (controller) {
         controller.abort();
    }
    controller = new AbortController();
    const signal = controller.signal;
    batch(() =&amp;gt; {
       loading.value = true;
       errorMessage.value = &apos;&apos;;
       postData.value = {};
    });
    try {
       fetch(`/posts/${postId.value}`, { signal }).then(res =&amp;gt; res.json()).then(post =&amp;gt; {
            batch(() =&amp;gt; {
                 postData.value = post;
                 loading.value = false;
             });
        }); 
    } catch (err) {
        errorMessage.value = err.message;
    }
});
effect(() =&amp;gt;  {
    if (loading.value) {
        el.innerText = &apos;Loading...&apos;;
    } else if (errorMessage.value) {
        el.innerText = `Error: ${errorMessage.value}`;
    } else {
        el.innerText = postData.value.title;
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can show the proper states, but this is only for one request...&lt;/p&gt;
&lt;p&gt;We could wrap this up in a class to reuse or create a new type of signal that can work with asynchronous data.&lt;/p&gt;
&lt;h2&gt;AsyncState&lt;/h2&gt;
&lt;p&gt;We want to have a base class that we can make our loading states easily extend from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export class AsyncState&amp;lt;T&amp;gt; {
  constructor() {}

  get value(): T | null {
    return null;
  }

  get requireValue(): T {
    throw new Error(&amp;quot;Value not set&amp;quot;);
  }

  get error(): any {
    return null;
  }

  get isLoading(): boolean {
    return false;
  }

  get hasValue(): boolean {
    return false;
  }

  get hasError(): boolean {
    return false;
  }

  map&amp;lt;R&amp;gt;(builders: {
    onLoading: () =&amp;gt; R;
    onError: (error: any) =&amp;gt; R;
    onData: (data: T) =&amp;gt; R;
  }): R {
    if (this.hasError) {
      return builders.onError(this.error);
    }
    if (this.hasValue) {
      return builders.onData(this.requireValue);
    }
    return builders.onLoading();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://dartsignals.dev/async/state/&quot;&gt;This class&lt;/a&gt; actually comes from a &lt;a href=&quot;https://github.com/rodydavis/signals.dart&quot;&gt;Dart port of preact signals&lt;/a&gt; I created.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This allows us to easily check if there is an actual value, error or if it is loading. It also provides an easy builder method to map the state to another value. 🤩&lt;/p&gt;
&lt;h3&gt;AsyncData&lt;/h3&gt;
&lt;p&gt;The loading state extends &lt;strong&gt;AsyncState&lt;/strong&gt; and passes the value in the constructor to the overriden methods.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export class AsyncData&amp;lt;T&amp;gt; extends AsyncState&amp;lt;T&amp;gt; {
  private _value: T;

  constructor(value: T) {
    super();
    this._value = value;
  }

  get requireValue(): T {
    return this._value;
  }

  get hasValue(): boolean {
    return true;
  }

  toString() {
    return `AsyncData{${this._value}}`;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;AsyncLoading&lt;/h3&gt;
&lt;p&gt;For the loading state we override the methods like &lt;strong&gt;AsyncData&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export class AsyncLoading&amp;lt;T&amp;gt; extends AsyncState&amp;lt;T&amp;gt; {
  get value(): T | null {
    return null;
  }

  get isLoading(): boolean {
    return true;
  }

  toString() {
    return `AsyncLoading{}`;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;AsyncError&lt;/h3&gt;
&lt;p&gt;For the error state we can pass an object of any type to return the error as value instead of throwing an exception (like Go).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export class AsyncError&amp;lt;T&amp;gt; extends AsyncState&amp;lt;T&amp;gt; {
  private _error: any;

  constructor(error: any) {
    super();
    this._error = error;
  }

  get error(): any {
    return this._error;
  }

  get hasError(): boolean {
    return true;
  }

  toString() {
    return `AsyncError{${this._error}}`;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;asyncSignal&lt;/h2&gt;
&lt;p&gt;Now we the state classes created, we can create a function to create an asynchronous signal with all the logic we talked about earlier.&lt;/p&gt;
&lt;p&gt;We need to show the sync value at any time and have a way to abort previous requests.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export function asyncSignal&amp;lt;T&amp;gt;(
  cb: () =&amp;gt; Promise&amp;lt;T&amp;gt;
): ReadonlySignal&amp;lt;AsyncState&amp;lt;T&amp;gt;&amp;gt; {
  const loading = new AsyncLoading&amp;lt;T&amp;gt;();
  const reset = Symbol(&amp;quot;reset&amp;quot;);
  const s = signal&amp;lt;AsyncState&amp;lt;T&amp;gt;&amp;gt;(loading);
  const c = computed&amp;lt;Promise&amp;lt;T&amp;gt;&amp;gt;(cb);
  let controller: AbortController | null;
  let abortSignal: AbortSignal | null;

  function execute(cb: Promise&amp;lt;T&amp;gt;, cancel: AbortSignal) {
    (async () =&amp;gt; {
      s.value = loading;
      try {
        const result = await new Promise&amp;lt;T&amp;gt;(async (resolve, reject) =&amp;gt; {
          if (cancel.aborted) {
            reject(cancel.reason);
          }
          cancel.addEventListener(&amp;quot;abort&amp;quot;, () =&amp;gt; {
            reject(cancel.reason);
          });
          try {
            const result = await cb;
            if (cancel.aborted) {
              reject(cancel.reason);
              return;
            }
            resolve(result);
          } catch (error) {
            reject(error);
          }
        });
        s.value = new AsyncData&amp;lt;T&amp;gt;(result);
      } catch (error) {
        if (error === reset) {
          s.value = loading;
        } else {
          s.value = new AsyncError&amp;lt;T&amp;gt;(error);
        }
      }
    })();
  }

  effect(() =&amp;gt; {
    if (controller != null) {
      controller.abort(reset);
    }
    controller = new AbortController();
    abortSignal = controller.signal;
    execute(c.value, abortSignal);
  });

  return s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it very easy to create multiple asynchronous signals and also use it anywhere else you have signals in the application like effects and computeds.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const el = document.querySelector(&apos;#output&apos;);
const postId =  signal(&apos;123&apos;);

const result = asyncSignal(() =&amp;gt; fetch(`/posts/${postId.value}`).then(res =&amp;gt; res.json()));

effect(() =&amp;gt; {
   el.innerText = result.value.map({
      onLoading: () =&amp;gt; &apos;Loading...&apos;,
      onError: (err) =&amp;gt; `Error: ${err}`,
      onData: (post) =&amp;gt; post.title, 
   });
});

postId.value = &apos;456&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I have started a Preact Signals GitHub discussion &lt;a href=&quot;https://github.com/preactjs/signals/discussions/648&quot;&gt;here&lt;/a&gt; and you can find a gist with the &lt;a href=&quot;https://gist.github.com/rodydavis/3b5266da2cc07f6574d425f5ce6e1e31&quot;&gt;final source code here&lt;/a&gt;. 🎉&lt;/p&gt;
&lt;p&gt;This has made working with asynchronous data a lot eaiser to work with and would love to hear your thoughts about ways to improve it 👀&lt;/p&gt;
&lt;p&gt;Also if you are curious about how Angular does asynchronous signals you can check out the &lt;a href=&quot;https://angular.dev/guide/signals/resource&quot;&gt;resource signal&lt;/a&gt; and the &lt;a href=&quot;https://justangular.com/blog/building-computed-async-for-signals-in-angular&quot;&gt;computedFrom/Async signal&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Flutter Infinite Canvas</title><link>https://rodydavis.com/flutter/snippets/infinite-canvas/</link><guid isPermaLink="true">https://rodydavis.com/flutter/snippets/infinite-canvas/</guid><description>Build an infinite canvas in Flutter using InteractiveViewer and CustomMultiChildLayout for creating multi-touch canvases and dynamic layouts.</description><pubDate>Sun, 19 Jan 2025 05:33:00 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Infinite Canvas&lt;/h1&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;The following is an example of how to build an infinite canvas with &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/InteractiveViewer-class.html&quot;&gt;InteractiveViewer&lt;/a&gt; and &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/CustomMultiChildLayout-class.html&quot;&gt;CustomMultiChildLayout&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Blog post: &lt;a href=&quot;https://rodydavis.com/posts/flutter-multi-touch-canvas&quot;&gt;Create a multi touch canvas in Flutter&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:vector_math/vector_math_64.dart&apos; hide Colors;

void main() {
  final controller = WidgetCanvasController([
    WidgetCanvasChild(
      key: UniqueKey(),
      offset: Offset.zero,
      size: const Size(400, 800),
      child: Scaffold(
        appBar: AppBar(
          title: const Text(&apos;Scaffold 1&apos;),
        ),
        body: const Placeholder(),
      ),
    ),
    WidgetCanvasChild(
      key: UniqueKey(),
      offset: const Offset(200, 200),
      size: const Size(400, 800),
      child: Scaffold(
        appBar: AppBar(
          title: const Text(&apos;Scaffold 2&apos;),
        ),
        body: const Placeholder(),
      ),
    ),
  ]);
  runApp(MaterialApp(home: WidgetCanvas(controller: controller)));
}

class WidgetCanvas extends StatefulWidget {
  const WidgetCanvas({super.key, required this.controller});

  final WidgetCanvasController controller;

  @override
  State&amp;lt;WidgetCanvas&amp;gt; createState() =&amp;gt; WidgetCanvasState();
}

class WidgetCanvasState extends State&amp;lt;WidgetCanvas&amp;gt; {
  @override
  void initState() {
    super.initState();
    controller.addListener(onUpdate);
  }

  @override
  void dispose() {
    controller.removeListener(onUpdate);
    super.dispose();
  }

  void onUpdate() {
    if (mounted) setState(() {});
  }

  static const Size _gridSize = Size.square(50);
  WidgetCanvasController get controller =&amp;gt; widget.controller;

  Rect axisAlignedBoundingBox(Quad quad) {
    double xMin = quad.point0.x;
    double xMax = quad.point0.x;
    double yMin = quad.point0.y;
    double yMax = quad.point0.y;
    for (final Vector3 point in &amp;lt;Vector3&amp;gt;[
      quad.point1,
      quad.point2,
      quad.point3,
    ]) {
      if (point.x &amp;lt; xMin) {
        xMin = point.x;
      } else if (point.x &amp;gt; xMax) {
        xMax = point.x;
      }

      if (point.y &amp;lt; yMin) {
        yMin = point.y;
      } else if (point.y &amp;gt; yMax) {
        yMax = point.y;
      }
    }

    return Rect.fromLTRB(xMin, yMin, xMax, yMax);
  }

  @override
  Widget build(BuildContext context) {
    const inset = 2.0;
    return Listener(
      onPointerDown: (details) {
        controller.mouseDown = true;
        controller.checkSelection(details.localPosition);
      },
      onPointerUp: (details) {
        controller.mouseDown = false;
      },
      onPointerCancel: (details) {
        controller.mouseDown = false;
      },
      onPointerMove: (details) {},
      child: LayoutBuilder(
        builder: (context, constraints) =&amp;gt; InteractiveViewer.builder(
          transformationController: controller.transform,
          panEnabled: controller.canvasMoveEnabled,
          scaleEnabled: controller.canvasMoveEnabled,
          onInteractionStart: (details) {
            controller.mousePosition = details.focalPoint;
          },
          onInteractionUpdate: (details) {
            if (!controller.mouseDown) {
              controller.scale = details.scale;
            } else {
              controller.moveSelection(details.focalPoint);
            }
            controller.mousePosition = details.focalPoint;
          },
          onInteractionEnd: (details) {},
          minScale: 0.4,
          maxScale: 4,
          boundaryMargin: const EdgeInsets.all(double.infinity),
          builder: (context, viewport) {
            return SizedBox(
              width: 1,
              height: 1,
              child: Stack(
                clipBehavior: Clip.none,
                children: [
                  Positioned.fill(
                    child: GridBackgroundBuilder(
                      cellWidth: _gridSize.width,
                      cellHeight: _gridSize.height,
                      viewport: axisAlignedBoundingBox(viewport),
                    ),
                  ),
                  Positioned.fill(
                    child: CustomMultiChildLayout(
                      delegate: WidgetCanvasDelegate(controller),
                      children: controller.children.map((e) {
                        return LayoutId(
                            id: e,
                            child: Stack(
                              clipBehavior: Clip.none,
                              children: [
                                Positioned.fill(
                                  child: Material(
                                    elevation: 4,
                                    child: SizedBox.fromSize(
                                      size: e.size,
                                      child: e.child,
                                    ),
                                  ),
                                ),
                                if (controller.isSelected(e.key!))
                                  Positioned.fill(
                                    top: -inset,
                                    left: -inset,
                                    right: -inset,
                                    bottom: -inset,
                                    child: Container(
                                      decoration: BoxDecoration(
                                        border: Border.all(
                                          color: Colors.blue,
                                          width: 1,
                                        ),
                                      ),
                                    ),
                                  ),
                              ],
                            ));
                      }).toList(),
                    ),
                  ),
                ],
              ),
            );
          },
        ),
      ),
    );
  }
}

class GridBackgroundBuilder extends StatelessWidget {
  const GridBackgroundBuilder({
    super.key,
    required this.cellWidth,
    required this.cellHeight,
    required this.viewport,
  });

  final double cellWidth;
  final double cellHeight;
  final Rect viewport;

  @override
  Widget build(BuildContext context) {
    final int firstRow = (viewport.top / cellHeight).floor();
    final int lastRow = (viewport.bottom / cellHeight).ceil();
    final int firstCol = (viewport.left / cellWidth).floor();
    final int lastCol = (viewport.right / cellWidth).ceil();

    return Stack(
      clipBehavior: Clip.none,
      children: &amp;lt;Widget&amp;gt;[
        for (int row = firstRow; row &amp;lt; lastRow; row++)
          for (int col = firstCol; col &amp;lt; lastCol; col++)
            Positioned(
              left: col * cellWidth,
              top: row * cellHeight,
              child: Container(
                height: cellHeight,
                width: cellWidth,
                decoration: BoxDecoration(
                  border: Border.all(
                    color: Colors.grey.withOpacity(0.1),
                    width: 1,
                  ),
                ),
              ),
            ),
      ],
    );
  }
}

class WidgetCanvasDelegate extends MultiChildLayoutDelegate {
  WidgetCanvasDelegate(this.controller);
  final WidgetCanvasController controller;
  List&amp;lt;WidgetCanvasChild&amp;gt; get children =&amp;gt; controller.children;

  Size backgroundSize = const Size(100000, 100000);
  late Offset backgroundOffset = Offset(
    -backgroundSize.width / 2,
    -backgroundSize.height / 2,
  );

  @override
  void performLayout(Size size) {
    // Then draw the screens.
    for (final widget in children) {
      layoutChild(widget, BoxConstraints.tight(widget.size));
      positionChild(widget, widget.offset);
    }
  }

  @override
  bool shouldRelayout(WidgetCanvasDelegate oldDelegate) =&amp;gt; true;
}

class WidgetCanvasChild extends StatelessWidget {
  const WidgetCanvasChild({
    required Key key,
    required this.size,
    required this.offset,
    required this.child,
  }) : super(key: key);

  final Size size;
  final Offset offset;
  final Widget child;

  Rect get rect =&amp;gt; offset &amp;amp; size;

  WidgetCanvasChild copyWith({
    Size? size,
    Offset? offset,
    Widget? child,
  }) {
    return WidgetCanvasChild(
      key: key!,
      size: size ?? this.size,
      offset: offset ?? this.offset,
      child: child ?? this.child,
    );
  }

  @override
  Widget build(BuildContext context) {
    return child;
  }
}

class WidgetCanvasController extends ChangeNotifier {
  WidgetCanvasController(this.children);

  final List&amp;lt;WidgetCanvasChild&amp;gt; children;
  final Set&amp;lt;Key&amp;gt; _selected = {};
  late final transform = TransformationController();
  Matrix4 get matrix =&amp;gt; transform.value;
  double scale = 1;
  Offset mousePosition = Offset.zero;

  bool _mouseDown = false;
  bool get mouseDown =&amp;gt; _mouseDown;
  set mouseDown(bool value) {
    _mouseDown = value;
    notifyListeners();
  }

  bool isSelected(Key key) =&amp;gt; _selected.contains(key);

  bool get hasSelection =&amp;gt; _selected.isNotEmpty;

  bool get canvasMoveEnabled =&amp;gt; !mouseDown;

  Offset toLocal(Offset global) {
    return transform.toScene(global);
  }

  void checkSelection(Offset localPosition) {
    final offset = toLocal(localPosition);
    final selection = &amp;lt;Key&amp;gt;[];
    for (final child in children) {
      final rect = child.rect;
      if (rect.contains(offset)) {
        selection.add(child.key!);
      }
    }
    if (selection.isNotEmpty) {
      setSelection({selection.last});
    } else {
      deselectAll();
    }
  }

  void moveSelection(Offset position) {
    final delta = toLocal(position) - toLocal(mousePosition);
    for (final key in _selected) {
      final index = children.indexWhere((e) =&amp;gt; e.key == key);
      if (index == -1) continue;
      final current = children[index];
      children[index] = current.copyWith(
        offset: current.offset + delta,
      );
    }
    mousePosition = position;
    notifyListeners();
  }

  void select(Key key) {
    _selected.add(key);
    notifyListeners();
  }

  void setSelection(Set&amp;lt;Key&amp;gt; keys) {
    _selected.clear();
    _selected.addAll(keys);
    notifyListeners();
  }

  void deselect(Key key) {
    _selected.remove(key);
    notifyListeners();
  }

  void deselectAll() {
    _selected.clear();
    notifyListeners();
  }

  void add(WidgetCanvasChild child) {
    children.add(child);
    notifyListeners();
  }

  void remove(Key key) {
    children.removeWhere((e) =&amp;gt; e.key == key);
    notifyListeners();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
</content:encoded></item><item><title>Flutter Input Output Preview</title><link>https://rodydavis.com/flutter/snippets/input-output-preview/</link><guid isPermaLink="true">https://rodydavis.com/flutter/snippets/input-output-preview/</guid><description>Flutter widget for creating a two-pane layout with customizable primary and secondary content, title, actions, and dark mode support.</description><pubDate>Sun, 19 Jan 2025 05:35:39 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Input Output Preview&lt;/h1&gt;
&lt;p&gt;First we need a two pane widget to properly render on mobile and desktop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

class TwoPane extends StatefulWidget {
  const TwoPane({
    super.key,
    required this.primary,
    required this.secondary,
    required this.title,
    this.actions = const [],
    this.loading = false,
  });

  final (String, WidgetBuilder) primary, secondary;
  final List&amp;lt;Widget&amp;gt; actions;
  final String title;
  final bool loading;

  @override
  State&amp;lt;TwoPane&amp;gt; createState() =&amp;gt; _TwoPaneState();
}

class _TwoPaneState extends State&amp;lt;TwoPane&amp;gt; {
  bool darkMode = false;

  void toggleDarkMode() {
    if (mounted) {
      setState(() {
        darkMode = !darkMode;
      });
    }
  }

  ThemeData theme(Color color, Brightness brightness) {
    return ThemeData(
      brightness: brightness,
      colorScheme: ColorScheme.fromSeed(
        seedColor: color,
        brightness: brightness,
      ),
      useMaterial3: true,
    );
  }

  @override
  void didUpdateWidget(covariant TwoPane oldWidget) {
    if (oldWidget.loading != widget.loading ||
        oldWidget.title != widget.title ||
        oldWidget.actions != widget.actions) {
      if (mounted) setState(() {});
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: theme(Colors.purple, darkMode ? Brightness.dark : Brightness.light),
      child: Builder(builder: (context) {
        final (primaryTitle, primaryBuilder) = widget.primary;
        final (secondaryTitle, secondaryBuilder) = widget.secondary;
        return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
              centerTitle: false,
              actions: [
                IconButton(
                  tooltip: &apos;Toggle dark mode&apos;,
                  onPressed: toggleDarkMode,
                  icon: Icon(darkMode ? Icons.light_mode : Icons.dark_mode),
                ),
                ...widget.actions,
              ],
            ),
            body: LayoutBuilder(
              builder: (context, dimens) {
                if (dimens.maxWidth &amp;gt; 800 &amp;amp;&amp;amp; dimens.maxHeight &amp;gt; 600) {
                  return Column(
                    children: [
                      if (widget.loading) const LinearProgressIndicator(),
                      Expanded(
                        child: Row(
                          children: [
                            Flexible(
                              flex: 1,
                              child: primaryBuilder(context),
                            ),
                            Flexible(
                              flex: 1,
                              child: secondaryBuilder(context),
                            ),
                          ],
                        ),
                      ),
                    ],
                  );
                }
                return DefaultTabController(
                  length: 2,
                  child: Column(
                    children: [
                      SizedBox(
                        height: kToolbarHeight,
                        width: double.infinity,
                        child: TabBar(
                          tabs: [
                            Tab(text: primaryTitle),
                            Tab(text: secondaryTitle),
                          ],
                        ),
                      ),
                      if (widget.loading) const LinearProgressIndicator(),
                      Expanded(
                        child: TabBarView(
                          children: [
                            primaryBuilder(context),
                            secondaryBuilder(context),
                          ],
                        ),
                      ),
                    ],
                  ),
                );
              },
            ));
      }),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we can pass some text fields for one pane to render an output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

import &apos;two_pane.dart&apos;;

class InputOutputPreview extends StatefulWidget {
  const InputOutputPreview({
    super.key,
    required this.title,
    required this.input,
    required this.output,
    required this.preview,
    required this.placeholder,
    this.actions = const [],
    this.codeTitle = &apos;Code&apos;,
    this.previewTitle = &apos;Preview&apos;,
    this.loading = false,
    this.lazy = false,
    this.previewSize = const Size(300, 700),
  });

  final (
    String,
    ValueChanged&amp;lt;(TextEditingController, TextEditingController)&amp;gt;
  ) input, output;
  final Widget? preview;
  final Widget placeholder;
  final String title;
  final List&amp;lt;Widget&amp;gt; actions;
  final String codeTitle, previewTitle;
  final Size? previewSize;
  final bool loading;
  final bool lazy;

  @override
  State&amp;lt;InputOutputPreview&amp;gt; createState() =&amp;gt; _InputOutputPreviewState();
}

class _InputOutputPreviewState extends State&amp;lt;InputOutputPreview&amp;gt; {
  final input = TextEditingController();
  final output = TextEditingController();
  String? lastInput;
  String? lastOutput;

  @override
  void initState() {
    super.initState();
    if (!widget.lazy) input.addListener(onInput);
    output.addListener(onOutput);
  }

  @override
  void dispose() {
    super.dispose();
    if (!widget.lazy) input.removeListener(onInput);
    output.removeListener(onOutput);
    input.dispose();
    output.dispose();
  }

  void onInput() {
    final (_, update) = widget.input;
    final str = input.text;
    if (lastInput == str) return;
    update((input, output));
    lastInput = str;
  }

  void onOutput() {
    final (_, update) = widget.output;
    final str = output.text;
    if (lastOutput == str) return;
    update((output, input));
    lastOutput = str;
  }

  @override
  Widget build(BuildContext context) {
    final (inputTitle, _) = widget.input;
    final (outputTitle, _) = widget.output;
    return TwoPane(
      title: widget.title,
      actions: widget.actions,
      loading: widget.loading,
      primary: (
        widget.codeTitle,
        (context) =&amp;gt; SizedBox(
              height: double.infinity,
              child: Column(
                children: [
                  Flexible(
                    child: Padding(
                      padding: const EdgeInsets.all(8),
                      child: Card(
                        child: ListTile(
                          title: Text(inputTitle),
                          subtitle: TextField(
                            maxLines: null,
                            controller: input,
                            expands: true,
                            decoration: InputDecoration(
                              isCollapsed: true,
                              border: InputBorder.none,
                              suffix: widget.lazy
                                  ? IconButton(
                                      onPressed: onInput,
                                      icon: const Icon(Icons.save),
                                      tooltip: &apos;Submit&apos;,
                                    )
                                  : null,
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                  Flexible(
                    child: Padding(
                      padding: const EdgeInsets.all(8),
                      child: Card(
                        child: ListTile(
                          title: Text(outputTitle),
                          subtitle: TextField(
                            maxLines: null,
                            controller: output,
                            expands: true,
                            decoration: const InputDecoration(
                              isCollapsed: true,
                              border: InputBorder.none,
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
      ),
      secondary: (
        widget.previewTitle,
        (context) =&amp;gt; Container(
              color: Theme.of(context).colorScheme.surfaceVariant,
              child: Builder(builder: (context) {
                if (widget.previewSize == null) {
                  return widget.preview ?? widget.placeholder;
                }
                return Center(
                  child: Material(
                    elevation: 8,
                    child: SizedBox.fromSize(
                      size: widget.previewSize,
                      child: widget.preview ?? widget.placeholder,
                    ),
                  ),
                );
              }),
            )
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Lightweight Flutter Animations</title><link>https://rodydavis.com/flutter/snippets/lightweight-animations/</link><guid isPermaLink="true">https://rodydavis.com/flutter/snippets/lightweight-animations/</guid><description>Flutter code demonstrating a lightweight animation widget that simplifies state management and UI updates by using a ticker and custom painting.</description><pubDate>Sun, 19 Jan 2025 05:45:19 GMT</pubDate><content:encoded>&lt;h1&gt;Lightweight Flutter Animations&lt;/h1&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;First we need to create the abstract class:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;abstract class AnimationWidget&amp;lt;T extends StatefulWidget&amp;gt; extends State&amp;lt;T&amp;gt;
    with SingleTickerProviderStateMixin {
  Duration elapsed = Duration.zero;
  Duration delta = Duration.zero;
  late final Ticker ticker;
  BoxConstraints constraints = const BoxConstraints.tightFor();

  @override
  void initState() {
    super.initState();
    ticker = createTicker((elapsed) {
      delta = elapsed - this.elapsed;
      this.elapsed = elapsed;
      update(elapsed);
      if (mounted) setState(() {});
    });
    ticker.start();
    WidgetsBinding.instance.addPostFrameCallback(start);
  }

  @override
  void dispose() {
    ticker.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, dimens) {
      constraints = dimens;
      return paint(context, dimens);
    });
  }

  void start(Duration time) {}

  void update(Duration time);

  Widget paint(BuildContext context, BoxConstraints constraints);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will let us replace &lt;code&gt;State&lt;/code&gt; with &lt;code&gt;AnimationWidget&lt;/code&gt; and not need to call &lt;code&gt;setState&lt;/code&gt; to rebuild the ui.&lt;/p&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;p&gt;For the example we need an inline canvas painter:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class InlinePainter extends CustomPainter {
  InlinePainter({
    required this.draw,
    super.repaint,
  });

  final void Function(Canvas canvas, Size size) draw;

  @override
  void paint(Canvas canvas, Size size) {
    draw(canvas, size);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) =&amp;gt; true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the example using the new widget class:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter/scheduler.dart&apos;;

class SimpleExample extends StatefulWidget {
  const SimpleExample({Key? key}) : super(key: key);

  @override
  State&amp;lt;SimpleExample&amp;gt; createState() =&amp;gt; _SimpleExampleState();
}

class _SimpleExampleState extends AnimationWidget&amp;lt;SimpleExample&amp;gt; {
  var x = 0.0;
  var y = 0.0;
  var z = 0.0;

  @override
  void update(Duration time) {
    final t = delta.inMilliseconds / 1000;
    x += t;
    y += t;
    z += t;
  }

  @override
  Widget paint(BuildContext context, BoxConstraints constraints) {
    return Material(
      child: Center(
        child: Container(
          width: 100,
          height: 100,
          transform: Matrix4.identity()
            ..rotateX(x)
            ..rotateY(y)
            ..rotateZ(z),
          child: const Text(
            &apos;Hello World&apos;,
            style: TextStyle(
              fontSize: 30,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
</content:encoded></item><item><title>Flutter Markdown View with Material 3</title><link>https://rodydavis.com/flutter/snippets/markdown-view-material-3/</link><guid isPermaLink="true">https://rodydavis.com/flutter/snippets/markdown-view-material-3/</guid><description>Style Flutter Markdown content with Material 3 text and color styles using the flutter_markdown package.</description><pubDate>Sun, 19 Jan 2025 05:37:02 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Markdown View with Material 3&lt;/h1&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;How to style the &lt;a href=&quot;https://pub.dev/packages/flutter_markdown&quot;&gt;Flutter markdown&lt;/a&gt; widget with &lt;a href=&quot;https://m3.material.io/&quot;&gt;Material 3&lt;/a&gt; text and color styles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter_markdown/flutter_markdown.dart&apos;;
import &apos;package:go_router/go_router.dart&apos;;
import &apos;package:markdown/markdown.dart&apos; as md;
import &apos;package:url_launcher/url_launcher.dart&apos;;

class MarkdownView extends StatelessWidget {
  const MarkdownView({
    Key? key,
    required this.markdown,
    this.textScaleFactor = 1,
  }) : super(key: key);

  final String markdown;
  final double textScaleFactor;

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;
    final textTheme = Theme.of(context).textTheme;
    return Markdown(
        data: markdown,
        selectable: true,
        softLineBreak: true,
        onTapLink: (text, link, _) {
          final url = link ?? &apos;/&apos;;
          if (url.startsWith(&apos;http&apos;)) {
            launchUrl(Uri.parse(url));
          } else {
            context.push(url);
          }
        },
        extensionSet: md.ExtensionSet(
          md.ExtensionSet.gitHubFlavored.blockSyntaxes,
          [md.EmojiSyntax(), ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes],
        ),
        styleSheet: MarkdownStyleSheet(
            textScaleFactor: textScaleFactor,
            p: textTheme.bodyLarge!.copyWith(
              fontSize: 16,
              color: colors.onSurface.withOpacity(0.72),
            ),
            a: TextStyle(
              decoration: TextDecoration.underline,
              color: colors.onSurface.withOpacity(0.72),
            ),
            h1: textTheme.displaySmall!.copyWith(
              fontSize: 25,
              color: colors.onSurface,
            ),
            h2: textTheme.headlineLarge!.copyWith(
              fontSize: 20,
              color: colors.onSurface,
            ),
            h3: textTheme.headlineMedium!.copyWith(
              fontSize: 18,
              color: colors.onSurface,
            ),
            h4: textTheme.headlineSmall!.copyWith(
              fontSize: 16,
              color: colors.onSurface,
            ),
            h5: textTheme.titleLarge!.copyWith(
              fontSize: 16,
              color: colors.onSurface,
            ),
            h6: textTheme.titleMedium!.copyWith(
              fontSize: 16,
              color: colors.onSurface,
            ),
            listBullet: textTheme.bodyLarge!.copyWith(
              color: colors.onSurface,
            ),
            em: const TextStyle(fontStyle: FontStyle.italic),
            strong: const TextStyle(fontWeight: FontWeight.bold),
            blockquote: TextStyle(
              fontStyle: FontStyle.italic,
              fontWeight: FontWeight.w500,
              color: colors.onSurfaceVariant,
            ),
            blockquoteDecoration: BoxDecoration(
              color: colors.surfaceVariant,
              borderRadius: BorderRadius.circular(4),
            ),
            code: const TextStyle(fontFamily: &apos;monospace&apos;),
            tableHead:
                const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
            tableBody:
                const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
            blockSpacing: 8,
            listIndent: 32,
            blockquotePadding: const EdgeInsets.all(8),
            h1Padding: const EdgeInsets.symmetric(vertical: 8),
            h2Padding: const EdgeInsets.symmetric(vertical: 8),
            h3Padding: const EdgeInsets.symmetric(vertical: 8),
            h4Padding: const EdgeInsets.symmetric(vertical: 8),
            h5Padding: const EdgeInsets.symmetric(vertical: 8),
            h6Padding: const EdgeInsets.symmetric(vertical: 8),
            codeblockPadding: const EdgeInsets.all(8),
            codeblockDecoration: BoxDecoration(
              borderRadius: BorderRadius.circular(4),
              color: colors.surfaceVariant,
            ),
            horizontalRuleDecoration: BoxDecoration(
              border: Border(
                top: BorderSide(
                  color: colors.outline.withOpacity(0.4),
                  width: 1,
                ),
              ),
            )),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Flutter Master-detail view</title><link>https://rodydavis.com/flutter/snippets/master-detail-view/</link><guid isPermaLink="true">https://rodydavis.com/flutter/snippets/master-detail-view/</guid><description>Build a Flutter master-detail view for mobile, desktop, and web applications, handling list rendering and detail screen navigation with a customizable widget.</description><pubDate>Sun, 19 Jan 2025 05:38:59 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Master-detail view&lt;/h1&gt;
&lt;p&gt;When building mobile, desktop and web applications with Flutter often times you are faced with what to do with lists and the content when selected. Depending on the data you may have a list that renders another list before resolving to a detail view. On tablet or desktop this can be achieved with multi-column layouts.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;On mobile you will still need to push to the details screen since the space is constrained.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;How to build a &lt;a href=&quot;https://en.wikipedia.org/wiki/Master%E2%80%93detail_interface&quot;&gt;Master-detail&lt;/a&gt; view with Flutter:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:flutter/material.dart&apos;;

class MasterDetail&amp;lt;T&amp;gt; extends StatefulWidget {
  const MasterDetail({
    Key? key,
    required this.listBuilder,
    required this.detailBuilder,
    required this.onPush,
    this.emptyBuilder,
  }) : super(key: key);

  final Widget Function(BuildContext, ValueChanged&amp;lt;T?&amp;gt;, T?) listBuilder;
  final Widget Function(BuildContext, T, bool) detailBuilder;
  final void Function(BuildContext, T) onPush;
  final WidgetBuilder? emptyBuilder;

  @override
  State&amp;lt;MasterDetail&amp;lt;T&amp;gt;&amp;gt; createState() =&amp;gt; _MasterDetailState&amp;lt;T&amp;gt;();
}

class _MasterDetailState&amp;lt;T&amp;gt; extends State&amp;lt;MasterDetail&amp;lt;T&amp;gt;&amp;gt; {
  final selected = ValueNotifier&amp;lt;T?&amp;gt;(null);
  double? detailsWidth;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      primary: false,
      body: LayoutBuilder(
        builder: (context, dimens) {
          const double minWidth = 350;
          final maxWidth = dimens.maxWidth - minWidth;
          if (detailsWidth != null) {
            if (detailsWidth! &amp;gt; maxWidth) {
              detailsWidth = maxWidth;
            }
            if (detailsWidth! &amp;lt; minWidth) {
              detailsWidth = minWidth;
            }
          }
          return ValueListenableBuilder&amp;lt;T?&amp;gt;(
            valueListenable: selected,
            builder: (context, item, child) {
              final canShowDetails = dimens.maxWidth &amp;gt; 800;
              final showDetails = item != null &amp;amp;&amp;amp; canShowDetails;
              return Row(
                children: [
                  Expanded(
                    child: widget.listBuilder(context, (item) {
                      if (canShowDetails) {
                        selected.value = item;
                      } else {
                        selected.value = null;
                        if (item != null) widget.onPush(context, item);
                      }
                    }, selected.value),
                  ),
                  if (canShowDetails)
                    MouseRegion(
                      cursor: SystemMouseCursors.resizeLeftRight,
                      child: GestureDetector(
                        behavior: HitTestBehavior.opaque,
                        onHorizontalDragUpdate: (details) {
                          if (mounted) {
                            setState(() {
                              double w = detailsWidth ?? maxWidth;
                              w -= details.delta.dx;
                              // Check for min width
                              if (w &amp;lt; minWidth) {
                                w = minWidth;
                              }
                              // Check for max width
                              if (w &amp;gt; maxWidth) {
                                w = maxWidth;
                              }
                              detailsWidth = w;
                            });
                          }
                        },
                        child: const SizedBox(
                          width: 5,
                          height: double.infinity,
                          child: VerticalDivider(),
                        ),
                      ),
                    ),
                  if (canShowDetails)
                    SizedBox(
                      width: detailsWidth ?? maxWidth,
                      height: double.infinity,
                      child: showDetails
                          ? widget.detailBuilder(context, item, false)
                          : widget.emptyBuilder?.call(context) ??
                              const Center(
                                child: Text(&apos;Select a item to view details&apos;),
                              ),
                    ),
                ],
              );
            },
          );
        },
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This widget will size itself after layout and try to size the list as small as possible with the details filling up the rest. This is important for later when we nest multiple of these to create progressively adapting layouts.&lt;/p&gt;
</content:encoded></item><item><title>Flutter Native HTTP Client</title><link>https://rodydavis.com/flutter/snippets/native-http-client/</link><guid isPermaLink="true">https://rodydavis.com/flutter/snippets/native-http-client/</guid><description>Flutter code demonstrating how to implement a native HTTP client that adapts to Android (using Cronet) and Cupertino (using CupertinoHTTP) platforms, leveraging device information for user agent customization.</description><pubDate>Sun, 19 Jan 2025 05:39:48 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Native HTTP Client&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;package:cronet_http/cronet_http.dart&apos;;
import &apos;package:cupertino_http/cupertino_http.dart&apos;;
import &apos;package:device_info_plus/device_info_plus.dart&apos;;
import &apos;package:flutter/material.dart&apos;;
import &apos;package:http/http.dart&apos;;
import &apos;package:platform_info/platform_info.dart&apos;;

void main() async {
  var clientFactory = Client.new;
  final device = DeviceInfoPlugin();
  if (platform.isAndroid) {
    final engine = CronetEngine.build(
      cacheMode: CacheMode.memory,
      userAgent: (await device.androidInfo).model,
    );
    clientFactory = () =&amp;gt; CronetClient.fromCronetEngine(engine);
  } else if (platform.isCupertino) {
    clientFactory = CupertinoClient.defaultSessionConfiguration.call;
  }
  runWithClient(() =&amp;gt; runApp(const MyApp()),clientFactory);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Flutter Stream Widget</title><link>https://rodydavis.com/flutter/snippets/stream-widget/</link><guid isPermaLink="true">https://rodydavis.com/flutter/snippets/stream-widget/</guid><description>Flutter StreamWidget allows building widgets directly from streams, providing a reactive approach for dynamic UI updates.</description><pubDate>Sun, 19 Jan 2025 05:41:20 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Stream Widget&lt;/h1&gt;
&lt;p&gt;Work with streams directly in the build method of a &lt;a href=&quot;https://flutter.dev/&quot;&gt;Flutter&lt;/a&gt; widget:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:async&apos;;

import &apos;package:flutter/widgets.dart&apos;;

abstract class StreamWidget extends StatefulWidget {
  const StreamWidget({Key? key}) : super(key: key);

  Stream&amp;lt;Widget&amp;gt; build(BuildContext context);

  void initState() {}

  void dispose() {}

  void reassemble() {}

  Widget? buildEmpty(BuildContext context) =&amp;gt; null;

  Widget? buildError(BuildContext context, Object? error) =&amp;gt; null;

  @override
  State&amp;lt;StreamWidget&amp;gt; createState() =&amp;gt; _StreamWidgetState();
}

class _StreamWidgetState extends State&amp;lt;StreamWidget&amp;gt; {
  @override
  void initState() {
    widget.initState.call();
    super.initState();
  }

  @override
  void dispose() {
    widget.dispose.call();
    super.dispose();
  }

  @override
  void reassemble() {
    widget.reassemble.call();
    super.reassemble();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: widget.build(context),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          final result = widget.buildError(context, snapshot.error);
          if (result != null) return result;
        }
        if (snapshot.hasData) {
          return snapshot.data!;
        } else {
          final result = widget.buildEmpty(context);
          if (result != null) return result;
        }
        return const SizedBox.shrink();
      },
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This could also be applied to Future widgets, but for reactive screens, streams are closer to what is actually happening.&lt;/p&gt;
&lt;h2&gt;Riverpod Example&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import &apos;dart:async&apos;;

import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter_riverpod/flutter_riverpod.dart&apos;;
import &apos;package:riverpod_annotation/riverpod_annotation.dart&apos;;

part &apos;generated.g.dart&apos;;

@riverpod
class GeneratedWidget extends _$GeneratedWidget {
  @override
  Widget build(BuildContext context) {
    return const Text(&apos;Generated widget!&apos;);
  }
}

@riverpod
class StreamWidget extends _$StreamWidget {
  @override
  Stream&amp;lt;Widget&amp;gt; build(BuildContext context) async* {
    final controller = StreamController&amp;lt;int&amp;gt;();
    final timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      controller.add(timer.tick);
    });
    yield* controller.stream.map((event) =&amp;gt; Text(&apos;Stream widget: $event&apos;));
    timer.cancel();
    await controller.close();
  }
}

@riverpod
class FutureWidget extends _$FutureWidget {
  @override
  Future&amp;lt;Widget&amp;gt; build(BuildContext context) async {
    await Future.delayed(const Duration(seconds: 3));
    return const Text(&apos;Future completed!&apos;);
  }
}

class Example extends StatelessWidget {
  const Example({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            Consumer(builder: (context, ref, child) {
              final generated = ref.watch(generatedWidgetProvider(context));
              return generated;
            }),
            Consumer(builder: (context, ref, child) {
              final stream = ref.watch(streamWidgetProvider(context));
              return stream.when(
                data: (data) =&amp;gt; data,
                error: (error, stack) =&amp;gt; Text(error.toString()),
                loading: () =&amp;gt; const CircularProgressIndicator(),
              );
            }),
            Consumer(builder: (context, ref, child) {
              final future = ref.watch(futureWidgetProvider(context));
              return future.when(
                data: (data) =&amp;gt; data,
                error: (error, stack) =&amp;gt; Text(error.toString()),
                loading: () =&amp;gt; const CircularProgressIndicator(),
              );
            }),
          ],
        ),
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Building the Flutter Widget Maker and Storyboard</title><link>https://rodydavis.com/podcast/creative-engineering/building-the-flutter-widget-maker-and-storyboard/</link><guid isPermaLink="true">https://rodydavis.com/podcast/creative-engineering/building-the-flutter-widget-maker-and-storyboard/</guid><description>Learn how to build a Flutter widget maker and storyboard in this video featuring Norbert Kozsir and Rody Davis, with links to their profiles and resources.</description><pubDate>Tue, 14 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building the Flutter Widget Maker and Storyboard&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/Edvo_MSrzrg&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Norbert Kozsir - @norbertkozsir&lt;/p&gt;
&lt;p&gt;https://twitter.com/norbertkozsir&lt;/p&gt;
&lt;p&gt;https://github.com/norbert515&lt;/p&gt;
&lt;p&gt;Rody Davis - @rodydavis&lt;/p&gt;
&lt;p&gt;https://twitter.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://github.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://youtube.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://rodydavis.com&lt;/p&gt;
&lt;p&gt;Our podcast player:&lt;/p&gt;
&lt;p&gt;https://rodydavis.github.io/creative_engineering/&lt;/p&gt;
&lt;p&gt;Follow on Twitter:&lt;/p&gt;
&lt;p&gt;https://twitter.com/CreativeEngShow&lt;/p&gt;
</content:encoded></item><item><title>Flutter Adaptive UI and Web Assembly</title><link>https://rodydavis.com/podcast/creative-engineering/flutter-adaptive-ui-and-web-assembly/</link><guid isPermaLink="true">https://rodydavis.com/podcast/creative-engineering/flutter-adaptive-ui-and-web-assembly/</guid><description>This video discusses Flutter&apos;s adaptive UI and WebAssembly capabilities, with links to resources like GitHub, CodePen, and relevant talks, presented by industry experts.</description><pubDate>Tue, 31 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Adaptive UI and Web Assembly&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/MsqAGkrKVnc&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;BigScreen: https://www.oculus.com/experiences/quest/2497738113633933/
Github free for teams: https://github.blog/2020-04-14-github-is-now-free-for-teams/
Codepen for Flutter: https://codepen.io/flutter
Flutter internals talk: https://www.youtube.com/watch?v=PnWxW21vDak
The Rust book: https://doc.rust-lang.org/book/&lt;/p&gt;
&lt;p&gt;Norbert Kozsir - @norbertkozsir&lt;/p&gt;
&lt;p&gt;https://twitter.com/norbertkozsir&lt;/p&gt;
&lt;p&gt;https://github.com/norbert515&lt;/p&gt;
&lt;p&gt;Rody Davis - @rodydavis&lt;/p&gt;
&lt;p&gt;https://twitter.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://github.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://youtube.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://rodydavis.com&lt;/p&gt;
&lt;p&gt;Our podcast player:&lt;/p&gt;
&lt;p&gt;https://rodydavis.github.io/creative_engineering/&lt;/p&gt;
&lt;p&gt;Follow on Twitter:&lt;/p&gt;
&lt;p&gt;https://twitter.com/CreativeEngShow&lt;/p&gt;
</content:encoded></item><item><title>Flutter Desktop/Web and VR</title><link>https://rodydavis.com/podcast/creative-engineering/flutter-desktopweb-and-vr/</link><guid isPermaLink="true">https://rodydavis.com/podcast/creative-engineering/flutter-desktopweb-and-vr/</guid><description>Flutter developers discuss the future of Flutter, VR experiments, code generation, and emerging technologies in this podcast featuring Norbert Kozsir and Rody Davis.</description><pubDate>Wed, 25 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Desktop/Web and VR&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/nyxBRXjONoo&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Looking into the future of Flutter development, our experiments with VR, our thoughts on Code Generation and passion about new tech.&lt;/p&gt;
&lt;p&gt;Norbert Kozsir - @norbertkozsir&lt;/p&gt;
&lt;p&gt;https://twitter.com/norbertkozsir&lt;/p&gt;
&lt;p&gt;https://github.com/norbert515&lt;/p&gt;
&lt;p&gt;Rody Davis - @rodydavis&lt;/p&gt;
&lt;p&gt;https://twitter.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://github.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://youtube.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://rodydavis.com&lt;/p&gt;
&lt;p&gt;Our podcast player:&lt;/p&gt;
&lt;p&gt;https://rodydavis.github.io/creative_engineering/
Follow on Twitter:&lt;/p&gt;
&lt;p&gt;https://twitter.com/CreativeEngShow&lt;/p&gt;
</content:encoded></item><item><title>Flutter State Management and Dart Nullability feat. Simon Lightfoot</title><link>https://rodydavis.com/podcast/creative-engineering/flutter-state-management-and-dart-nullability-feat-simon-lightfoot/</link><guid isPermaLink="true">https://rodydavis.com/podcast/creative-engineering/flutter-state-management-and-dart-nullability-feat-simon-lightfoot/</guid><description>This video features a discussion with Simon Lightfoot on Flutter state management and Dart nullability, hosted by Norbert Kozsir and Rody Davis, with links to their profiles and resources.</description><pubDate>Tue, 28 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter State Management and Dart Nullability feat. Simon Lightfoot&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/jgxblf5SW4U&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Simon Lightfoot&lt;/p&gt;
&lt;p&gt;https://twitter.com/devangelslondon&lt;/p&gt;
&lt;p&gt;https://devangels.london/&lt;/p&gt;
&lt;p&gt;Norbert Kozsir - @norbertkozsir&lt;/p&gt;
&lt;p&gt;https://twitter.com/norbertkozsir&lt;/p&gt;
&lt;p&gt;https://github.com/norbert515&lt;/p&gt;
&lt;p&gt;Rody Davis - @rodydavis&lt;/p&gt;
&lt;p&gt;https://twitter.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://github.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://youtube.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://rodydavis.com&lt;/p&gt;
&lt;p&gt;Our podcast player:&lt;/p&gt;
&lt;p&gt;https://rodydavis.github.io/creative_engineering/&lt;/p&gt;
&lt;p&gt;Follow on Twitter:&lt;/p&gt;
&lt;p&gt;https://twitter.com/CreativeEngShow&lt;/p&gt;
</content:encoded></item><item><title>Flutter Testing and AppStore Rejection</title><link>https://rodydavis.com/podcast/creative-engineering/flutter-testing-and-appstore-rejection/</link><guid isPermaLink="true">https://rodydavis.com/podcast/creative-engineering/flutter-testing-and-appstore-rejection/</guid><description>This video discusses Flutter testing strategies, common pitfalls leading to App Store rejections, and solutions for improving app quality and test coverage, including state management, UI testing, logging, and mocking.</description><pubDate>Tue, 07 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Testing and AppStore Rejection&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/LAcE2YfQT2Q&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Follow Up&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rody experiences with apple approval&lt;/li&gt;
&lt;li&gt;Rejections&lt;/li&gt;
&lt;li&gt;Recourse&lt;/li&gt;
&lt;li&gt;Options&lt;/li&gt;
&lt;li&gt;2 million people using flutter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Testing&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;State management and testing trade offs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UI logic and replaying capabilities&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Logging&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mocking&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Smoke Tests&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MVVM&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Firebase&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Filesystem&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Folder Structure / Layers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Packages that can be tested&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Code Coverage
&amp;quot;flutter test --coverage&amp;quot;&lt;/p&gt;
&lt;p&gt;https://codemagic.io/start/
https://sentry.io/welcome/&lt;br&gt;
https://pub.dev/packages/mock_cloud_firestore
To visually run widget tests: flutter run test/widget_test.dart
Suggested finders: Just tap anywhere when running a widget test using flutter run&lt;br&gt;
https://pub.dev/packages/device_preview&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Logging&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sentry&lt;/li&gt;
&lt;li&gt;Crashlytics&lt;/li&gt;
&lt;li&gt;Flutter Testing&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Flutter Driver&lt;/li&gt;
&lt;li&gt;Unit Tests&lt;/li&gt;
&lt;li&gt;Flutter Octopus&lt;/li&gt;
&lt;li&gt;Flutter Interact&lt;/li&gt;
&lt;li&gt;Flutter VR Testing&lt;/li&gt;
&lt;li&gt;Xcode testing, Android testing&lt;/li&gt;
&lt;li&gt;Flutter i18n Localization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Norbert Kozsir - @norbertkozsir&lt;/p&gt;
&lt;p&gt;https://twitter.com/norbertkozsir&lt;/p&gt;
&lt;p&gt;https://github.com/norbert515&lt;/p&gt;
&lt;p&gt;Rody Davis - @rodydavis&lt;/p&gt;
&lt;p&gt;https://twitter.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://github.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://youtube.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://rodydavis.com&lt;/p&gt;
&lt;p&gt;Our podcast player:&lt;/p&gt;
&lt;p&gt;https://rodydavis.github.io/creative_engineering/&lt;/p&gt;
&lt;p&gt;Follow on Twitter:&lt;/p&gt;
&lt;p&gt;https://twitter.com/CreativeEngShow&lt;/p&gt;
</content:encoded></item><item><title>Pro Putt by Top Golf feat. Jason Clevering</title><link>https://rodydavis.com/podcast/creative-engineering/pro-putt-by-top-golf-feat-jason-clevering/</link><guid isPermaLink="true">https://rodydavis.com/podcast/creative-engineering/pro-putt-by-top-golf-feat-jason-clevering/</guid><description>Top Golf&apos;s Pro Putt features insights from Norbert Kozsir and Rody Davis, offering resources and a podcast for improving putting skills.</description><pubDate>Tue, 21 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Pro Putt by Top Golf feat. Jason Clevering&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/TqAEM9Y2cuQ&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Pro Putt by Top Golf&lt;/p&gt;
&lt;p&gt;https://www.proputt.com/&lt;/p&gt;
&lt;p&gt;Norbert Kozsir - @norbertkozsir&lt;/p&gt;
&lt;p&gt;https://twitter.com/norbertkozsir&lt;/p&gt;
&lt;p&gt;https://github.com/norbert515&lt;/p&gt;
&lt;p&gt;Rody Davis - @rodydavis&lt;/p&gt;
&lt;p&gt;https://twitter.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://github.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://youtube.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://rodydavis.com&lt;/p&gt;
&lt;p&gt;Our podcast player:&lt;/p&gt;
&lt;p&gt;https://rodydavis.github.io/creative_engineering/&lt;/p&gt;
&lt;p&gt;Follow on Twitter:&lt;/p&gt;
&lt;p&gt;https://twitter.com/CreativeEngShow&lt;/p&gt;
</content:encoded></item><item><title>Talking about Riverpod with Remi Rousselet</title><link>https://rodydavis.com/podcast/creative-engineering/talking-about-riverpod-with-remi-rousselet/</link><guid isPermaLink="true">https://rodydavis.com/podcast/creative-engineering/talking-about-riverpod-with-remi-rousselet/</guid><description>A podcast episode featuring Remi Rousselet, Norbert Kozsir, and Rody Davis discussing Riverpod, with links to their social media, GitHub, and a podcast player.</description><pubDate>Mon, 04 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Talking about Riverpod with Remi Rousselet&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/qyfdowkmtAk&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Diving into the details about Riverpod!&lt;/p&gt;
&lt;h3&gt;Remi Rousselet&lt;/h3&gt;
&lt;p&gt;https://twitter.com/remi_rousselet&lt;/p&gt;
&lt;p&gt;https://github.com/rrousselGit&lt;/p&gt;
&lt;p&gt;Norbert Kozsir - @norbertkozsir&lt;/p&gt;
&lt;p&gt;https://twitter.com/norbertkozsir&lt;/p&gt;
&lt;p&gt;https://github.com/norbert515&lt;/p&gt;
&lt;h3&gt;Rody Davis - @rodydavis&lt;/h3&gt;
&lt;p&gt;https://twitter.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://github.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://youtube.com/rodydavis&lt;/p&gt;
&lt;p&gt;https://rodydavis.com&lt;/p&gt;
&lt;h3&gt;Our podcast player&lt;/h3&gt;
&lt;p&gt;https://rodydavis.github.io/creative_engineering/&lt;/p&gt;
&lt;h3&gt;Follow on Twitter:&lt;/h3&gt;
&lt;p&gt;https://twitter.com/CreativeEngShow&lt;/p&gt;
</content:encoded></item><item><title>Building a DND Agent with ADK, Antigravity, and Gemini 3</title><link>https://rodydavis.com/podcast/tech-walks/building-a-dnd-agent-with-adk-antigravity-and-gemini-3/</link><guid isPermaLink="true">https://rodydavis.com/podcast/tech-walks/building-a-dnd-agent-with-adk-antigravity-and-gemini-3/</guid><description>Learn how to build a D&amp;D campaign agent using Google&apos;s Agent Development Kit (ADK) and Gemini 3, incorporating AI agents for storytelling, rules enforcement, and critical feedback.</description><pubDate>Thu, 04 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building a DND Agent with ADK, Antigravity, and Gemini 3&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/B6TcfdpkmxU&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Nohe breaks down how he built a &amp;quot;Campaign Agent&amp;quot; for Dungeons &amp;amp; Dragons/Pathfinder. This system uses a loop of specialized agents—including a receptionist, storyteller, rules lawyer, and critic—to generate playable adventures grounded in actual rulebooks using Google Search grounding.&lt;/p&gt;
&lt;p&gt;We also explore the Agent Development Kit (ADK), discussing why we prefer it for building sequential and parallel agent loops, how it handles tools (including GitHub MCP), and how to visualize your agent&apos;s logic as a graph.&lt;/p&gt;
&lt;p&gt;In this episode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;00:00 - Intro: What we do at Google (Firebase, Genkit, GenUI)&lt;/li&gt;
&lt;li&gt;01:22 - The state of AI Agents&lt;/li&gt;
&lt;li&gt;02:34 - Building a D&amp;amp;D Campaign Agent&lt;/li&gt;
&lt;li&gt;07:45 - The Architecture: Storyteller, Rules Lawyer &amp;amp; The Critic&lt;/li&gt;
&lt;li&gt;09:58 - Why loops are better than single-shot prompts&lt;/li&gt;
&lt;li&gt;12:41 - What is the ADK (Agent Development Kit)?&lt;/li&gt;
&lt;li&gt;16:16 - Using GitHub MCP and Tools&lt;/li&gt;
&lt;li&gt;18:25 - &amp;quot;That sounds like a Cron Job&amp;quot;: When NOT to use AI&lt;/li&gt;
&lt;li&gt;20:30 - Testing Agents and Vertex AI optimization&lt;/li&gt;
&lt;li&gt;29:45 - Sneak Peek: Offline-first syncing with Better Auth &amp;amp; PowerSync&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Resources &amp;amp; Topics Mentioned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ADK: https://google.github.io/adk-docs/&lt;/li&gt;
&lt;li&gt;Firebase Genkit: https://firebase.google.com/products/genkit&lt;/li&gt;
&lt;li&gt;Model Context Protocol (MCP): https://modelcontextprotocol.io/&lt;/li&gt;
&lt;li&gt;Google Vertex AI: https://cloud.google.com/vertex-ai&lt;/li&gt;
&lt;li&gt;PowerSync (Offline-First Sync): https://www.powersync.com/&lt;/li&gt;
&lt;li&gt;Better Auth: https://www.better-auth.com/&lt;/li&gt;
&lt;li&gt;Pathfinder 2e (Archives of Nethys): https://2e.aonprd.com/&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hosts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nohe: Working on Firebase, Genkit, and ADK.&lt;/li&gt;
&lt;li&gt;Rody: Working on Cross-platform full-stack AI and Generative UI.#AI #GoogleCloud #Genkit #ADK #SoftwareEngineering #DND #Pathfinder&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Building with Antigravity, Gemini CLI, and Stitch</title><link>https://rodydavis.com/podcast/tech-walks/building-with-antigravity-gemini-cli-and-stitch/</link><guid isPermaLink="true">https://rodydavis.com/podcast/tech-walks/building-with-antigravity-gemini-cli-and-stitch/</guid><description>A podcast episode discussing AI development tools like Gemini CLI, Anti-Gravity, and Stiitch, covering topics such as workflow comparisons, dev environments, infrastructure setup, context engineering, and generative UI.</description><pubDate>Mon, 26 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building with Antigravity, Gemini CLI, and Stitch&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/09MTW1o42UI&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;In this next episode of our &amp;quot;untitled&amp;quot; podcast, Nohe and Rody take a &amp;quot;tech walk&amp;quot; to discuss the evolving landscape of AI development tools. We dive deep into the differences between the linear workflows of Gemini CLI and the asynchronous, project-level capabilities of Anti-Gravity.&lt;/p&gt;
&lt;p&gt;We also geek out on home lab setups—discussing the shift from Docker Compose to Kubernetes (K3s) on Raspberry Pi clusters—and share a game-changing workflow using NotebookLM to generate context files for your AI agents. Finally, we explore Stitch for generative UI, including how to instantly create shaders and animations from simple screenshots.&lt;/p&gt;
&lt;p&gt;Key Topics Discussed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gemini CLI vs. Anti-Gravity: Comparing agentic loops and managing complex refactors.&lt;/li&gt;
&lt;li&gt;Dev Environments: Configuring Neovim, Tmux, and using Git Worktrees to manage multiple AI tasks simultaneously.&lt;/li&gt;
&lt;li&gt;Infrastructure &amp;amp; DevOps: Running local K3s clusters, SSH tunneling with Anti-Gravity, and using AI to bridge the gap between Docker Compose and Kubernetes.&lt;/li&gt;
&lt;li&gt;Context Engineering: How to scrape documentation and use NotebookLM to write robust agent.md.&lt;/li&gt;
&lt;li&gt;Generative UI: Using Stitch to redesign legacy apps and generate complex Flutter shaders automatically.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Chapters:
00:00 - Intro &amp;amp; Gemini CLI Configurations
01:05 - Gemini CLI vs. Anti-Gravity: Linear vs. Async Workflows
03:45 - Anti-Gravity’s Agent Manager &amp;amp; Project-Level Tasks
06:30 - Managing Multiple Contexts with Git Worktrees
08:30 - Neovim, Tmux, and AI Integration
12:30 - SSH Tunneling &amp;amp; Remote Development with Anti-Gravity
15:10 - Home Lab: Raspberry Pi Clusters &amp;amp; K3s vs. Docker Swarm
19:45 - Using AI to Convert Docker Compose to Kubernetes
22:55 - The NotebookLM Workflow: Generating Agent Rules from Docs
31:30 - Stitch: Generative UI, Shaders, and App Redesigns
35:20 - Outro &amp;amp; Teaser: Prompt Optimization&lt;/p&gt;
&lt;p&gt;Hosts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nohe: Working on Firebase, Genkit, and ADK.&lt;/li&gt;
&lt;li&gt;Rody: Working on Antigravity, Flutter, and Firebase Studio&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;#AI #GoogleCloud #GeminiCLI #Antigravity #SoftwareEngineering #NeoVim #Docker&lt;/p&gt;
</content:encoded></item><item><title>Talking about Skills, Optimizing Prompts and building MCP servers in apps</title><link>https://rodydavis.com/podcast/tech-walks/talking-about-skills-optimizing-prompts-and-building-mcp-servers-in-apps/</link><guid isPermaLink="true">https://rodydavis.com/podcast/tech-walks/talking-about-skills-optimizing-prompts-and-building-mcp-servers-in-apps/</guid><description>This video explores advanced AI development techniques including Agent Skills, prompt optimization using Vertex AI, and building Model Context Protocol (MCP) servers for Flutter applications.</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Talking about Skills, Optimizing Prompts and building MCP servers in apps&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/euyGjhHNP8w&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;We’re diving deep into the latest paradigms in AI development, starting with the difference between traditional context files (Gemini.md) and the new &amp;quot;Agent Skills&amp;quot; dynamic.&lt;/p&gt;
&lt;p&gt;We also share a story about using the Vertex AI Prompt Optimizer to automate our YouTube descriptions. It took 5 hours and nearly 100 million tokens, but the results were surprisingly consistent. Finally, we geek out on the Model Context Protocol (MCP), experimenting with exposing Flutter application state as local tools using Unix sockets.&lt;/p&gt;
&lt;p&gt;In this episode:&lt;/p&gt;
&lt;p&gt;0:00 - Intro: Why we are sitting today.
0:30 - What are Agent Skills? (Anthropic &amp;amp; Antigravity support) .
1:49 - Skills vs. Context: The difference between lazy loading and concatenating context files.
4:59 - How the API discovers and uses Skills.
7:25 - The open ecosystem for sharing and installing Skills.
10:30 - Deep Dive: Using Vertex AI Prompt Optimizer for YouTube descriptions.
13:28 - Converting CSVs to JSONL for data-driven optimization.
15:25 - The 100 Million Token experiment.
19:20 - Exploring MCP (Model Context Protocol) with Flutter.
22:03 - Using Unix Sockets vs. HTTP for local app control.
24:25 - Implementing the MCP spec in Antigravity.&lt;/p&gt;
&lt;p&gt;Resources mentioned:&lt;/p&gt;
&lt;p&gt;Open Skills Domain: https://agentskills.io/&lt;/p&gt;
&lt;p&gt;Vertex AI Prompt Optimizer (Colab Notebooks): https://cloud.google.com/blog/products/ai-machine-learning/announcing-vertex-ai-prompt-optimizer&lt;/p&gt;
&lt;p&gt;Boost accuracy with the prompt optimizer blog: https://firebase.blog/posts/2026/01/boost-accuracy-with-the-prompt-optimizer&lt;/p&gt;
&lt;p&gt;Connect with us: Let us know what you want us to talk about next!&lt;/p&gt;
&lt;p&gt;#AI #Gemini #AgentSkills #MCP #Flutter #VertexAI #PromptEngineering #TechPodcast&lt;/p&gt;
</content:encoded></item><item><title>Gemma now available in KerasNLP collection, Signal Input in developer preview, and more dev news!</title><link>https://rodydavis.com/videos/ai/gemma-now-available-in-kerasnlp-collection-signal-input-in-developer-preview-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/ai/gemma-now-available-in-kerasnlp-collection-signal-input-in-developer-preview-and-more-dev-news/</guid><description>Google Developer News Show: Gemma model in KerasNLP, Signal inputs preview for Angular, new metadata format Croissant, and MediaPipe LLaM inference/image generation support.</description><pubDate>Tue, 12 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Gemma now available in KerasNLP collection, Signal Input in developer preview, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/k5DouVmByEQ&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 381  | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Intro
0:08 - Signal inputs now in preview for Angular devs! → https://goo.gle/4a7Pdv5
0:39 - Gemma is now available in the KerasNLP collection → https://goo.gle/43gi8uA&lt;br&gt;
1:02 - Croissant, a new metadata format for ML datasets → https://goo.gle/3PgtGbG
1:49 - MediaPipe now supports LLM inference and image generation → https://goo.gle/3v3y331&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody Davis.Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google for Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more The Developer Show → https://goo.gle/GDevShow
Subscribe to Google for Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis;
Products Mentioned: Material Design - General, Angular - General;&lt;/p&gt;
</content:encoded></item><item><title>Google Computer Use model, Gemini CLI extensions, and more! - Google Developer News October 2025</title><link>https://rodydavis.com/videos/ai/google-computer-use-model-gemini-cli-extensions-and-more-google-developer-news-october-2025/</link><guid isPermaLink="true">https://rodydavis.com/videos/ai/google-computer-use-model-gemini-cli-extensions-and-more-google-developer-news-october-2025/</guid><description>Google Developer News October 2025 covers new AI features like the Gemini 2.5 Computer Use model, Gemini CLI Extensions, the Agent Development Kit (ADK), and upcoming hackathons and DevFest 2025.</description><pubDate>Thu, 23 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Google Computer Use model, Gemini CLI extensions, and more! - Google Developer News October 2025&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/BWX8cnMTq7E&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Welcome to this episode of Google Developer News – the October Edition. This month we dive into new features in the AI realm that include the Google Computer Use model, Gemini CLI Extensions, and the Agent Development Kit (ADK). Want to get involved? We also explore upcoming hackathons—AI Accelerate, the Chrome Built-in AI Challenge, and the Cloud Run Hackathon, while preparing for DevFest 2025. Register for these events below.&lt;/p&gt;
&lt;p&gt;Chapters:
0:00 - Introduction
0:12 - Google Computer Use Model
0:39 - Gemini CLI Extensions
1:18 - Jules Tools
1:53 - Agent Development Kit
2:56 - Hackathon opportunities
3:02 - AI Accelerate Hackathon
3:40 - Chrome Built-in AI Challenge
4:21 - Cloud Run Hackathon
5:00 - DevFest 2025
5:43 - Wrap up&lt;/p&gt;
&lt;p&gt;Resources:
Introducing the Gemini 2.5 Computer Use model → https://goo.gle/4o6qzm5
Now open for building: Introducing Gemini CLI extensions → https://goo.gle/47Cb3bI
Meet Jules Tools: A Command Line Companion for Google’s Async Coding Agent → https://goo.gle/47DVArC
Delight users by combining ADK Agents with Fancy Frontends using AG-UI  → https://goo.gle/4qp2mc4
AI Accelerate Hackathon → https://goo.gle/49gBx3G
Built in AI Challenge → https://goo.gle/4hude4C
Cloud Runathon → https://goo.gle/48W0dya&lt;/p&gt;
&lt;p&gt;Watch more Google Developer News → https://goo.gle/4e8Rysd&lt;br&gt;
Subscribe to Google for Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#GoogleDeveloperNews&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned: Gemini, Agent Developer Kit (ADK)&lt;/p&gt;
</content:encoded></item><item><title>How to Automate PR Summaries with Opal AI</title><link>https://rodydavis.com/videos/ai/how-to-automate-pr-summaries-with-opal-ai/</link><guid isPermaLink="true">https://rodydavis.com/videos/ai/how-to-automate-pr-summaries-with-opal-ai/</guid><description>Learn how to use Opal AI from Google Labs to automate pull request summarization with a user-friendly visual workflow editor and instant hosting.</description><pubDate>Mon, 12 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to Automate PR Summaries with Opal AI&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/gphcuJu8iHo&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Uncover how Opal from Google Labs revolutionizes building AI systems with its intuitive natural language and visual workflow editor. This tutorial guides you through creating an AI-powered mini-app, Git-Clarity, designed to summarize GitHub pull requests efficiently. Understand Opal’s multi-step workflow creation, fast iteration capabilities, and instant hosting features, making AI development accessible for everyone.&lt;/p&gt;
&lt;p&gt;Resources:
Learn more → https://goo.gle/3Yx4aDq&lt;/p&gt;
&lt;p&gt;Chapters:
0:00 - Introduction to Opal from Google Labs
1:06 - Opal’s intelligent multi-step workflows
2:10 - Exploring the generated workflow steps
3:29 - Verifying the complete workflow
4:52 - Iteration in practice: Refining output
5:25 - Sharing your live prototype
5:35 - Closing thoughts and future possibilities with Opal&lt;/p&gt;
&lt;p&gt;Subscribe to Google for Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned:  Google AI&lt;/p&gt;
</content:encoded></item><item><title>How to connect interfaces to the cloud with AI agents</title><link>https://rodydavis.com/videos/ai/how-to-connect-interfaces-to-the-cloud-with-ai-agents/</link><guid isPermaLink="true">https://rodydavis.com/videos/ai/how-to-connect-interfaces-to-the-cloud-with-ai-agents/</guid><description>Learn how to use Google&apos;s Agent Development Kit (ADK) and GenUI SDK for Flutter to build applications that connect to cloud services, databases, and internal APIs using AI agents.</description><pubDate>Wed, 17 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to connect interfaces to the cloud with AI agents&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/Dum-LYLm0Uw&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Verdure example → https://goo.gle/3XY7GX8
GenUI package → https://goo.gle/4oXUoou&lt;/p&gt;
&lt;p&gt;Give AI agents massive reasoning models, real-time APIs, and the ability to perform complex tasks. Watch along as Googler Rody Davis leverages Agent Development Kit (ADK) to securely access databases, call internal APIs, and orchestrate complex workflows. Watch along and discover the power of GenUI SDK for Flutter.&lt;/p&gt;
&lt;p&gt;Chapters:
0:00 - Intro
0:36 - What is Agent Development Kit?
1:17 - Rendering the UI for app workflows
1:56 -[Demo] Verdure
2:44 - [Demo] Building a new “Budget Slider”
4:32 - Summary&lt;/p&gt;
&lt;p&gt;Watch more Flutter Flight Plans → https://www.youtube.com/watch?v=pMoUg3dkDJk&amp;amp;list=PLjxrf2q8roU1yXu4k7ivSLAa0cizD4feH
🔔 Subscribe to Flutter → https://goo.gle/FlutterYT&lt;/p&gt;
&lt;p&gt;#Flutter&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned: Agent Development Kit, GenUI SDK for Flutter&lt;/p&gt;
</content:encoded></item><item><title>Leverage Gemini in your Android apps, Global Gamer Challenge, and more dev news!</title><link>https://rodydavis.com/videos/ai/leverage-gemini-in-your-android-apps-global-gamer-challenge-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/ai/leverage-gemini-in-your-android-apps-global-gamer-challenge-and-more-dev-news/</guid><description>Google Developer News Show: Learn about leveraging Gemini in Android apps, the Global Gamers Challenge, and other developer updates.</description><pubDate>Tue, 16 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Leverage Gemini in your Android apps, Global Gamer Challenge, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/2HrFs0Lfb-w&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 373  | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Intro
0:09 - Leverage Gemini in your Android apps → https://goo.gle/4aZCnjN
0:51 - Announcing the Global Gamers Challenge → https://goo.gle/4aVHAZZ
1:23 - Solution Challenge 2024 → https://goo.gle/4b2gbpp&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody Davis. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google for Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow
Subscribe to Google for Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;Products Mentioned: Gemini Nano&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned: Google AI, Gemini&lt;/p&gt;
</content:encoded></item><item><title>Offline vector search with SQLite and EmbeddingGemma</title><link>https://rodydavis.com/videos/ai/offline-vector-search-with-sqlite-and-embeddinggemma/</link><guid isPermaLink="true">https://rodydavis.com/videos/ai/offline-vector-search-with-sqlite-and-embeddinggemma/</guid><description>Learn to build an offline Retrieval-Augmented Generation (RAG) system using SQLite, embeddings, and Google&apos;s Gemma models for browser-based document querying, as demonstrated by Google&apos;s Rody Davis.</description><pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Offline vector search with SQLite and EmbeddingGemma&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/sobPBztQTtU&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Learn from Rody Davis, Senior Developer Relations Engineer at Google, how to query and embed documents using SQLite and embeddings with EmbeddingGemma and Gemma3. Create an offline RAG system that runs in the browser offline.&lt;/p&gt;
&lt;p&gt;Resources:
Github → https://goo.gle/4p2b3b1&lt;/p&gt;
&lt;p&gt;See more Web AI talks →  https://goo.gle/web-ai&lt;br&gt;
Subscribe to Chrome for Developers → https://goo.gle/ChromeDevs&lt;/p&gt;
&lt;p&gt;Event: Web AI Summit 2025&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned: AI for the web, Gemma 3&lt;/p&gt;
&lt;p&gt;#ChromeforDevelopers #WebAI&lt;/p&gt;
</content:encoded></item><item><title>The Gemini Cookbook, Popover API lands in Baseline, and more dev news!</title><link>https://rodydavis.com/videos/ai/the-gemini-cookbook-popover-api-lands-in-baseline-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/ai/the-gemini-cookbook-popover-api-lands-in-baseline-and-more-dev-news/</guid><description>Google Developer News Show: Gemini Cookbook, Popover API in Baseline, CI/CD privacy, and AI-powered build repair updates.</description><pubDate>Mon, 29 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;The Gemini Cookbook, Popover API lands in Baseline, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/oRjt_mIm6IY&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 388  | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Intro
0:19 - Popover API lands in Baseline → https://goo.gle/4a3qs2K
0:44 - Achieving privacy compliance with your CI/CD → https://goo.gle/4a6n0Vi
1:24 - Safely repairing  broken builds with ML&lt;br&gt;
1:54 - Improving Gboard language models via private federated analytics
2:37 - Gemini Cookbook → https://goo.gle/44jp9vq&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody Davis.Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google for Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more The Developer Show → https://goo.gle/GDevShow
Subscribe to Google for Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis;
Products Mentioned: ‎Gemini, Web Stories - General;&lt;/p&gt;
</content:encoded></item><item><title>Flutter &amp; Antigravity | Observable Flutter #76</title><link>https://rodydavis.com/videos/antigravity/flutter-antigravity-observable-flutter-76/</link><guid isPermaLink="true">https://rodydavis.com/videos/antigravity/flutter-antigravity-observable-flutter-76/</guid><description>Flutter developers discuss Google&apos;s new AntiGravity IDE for Flutter development in this video.</description><pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter &amp;amp; Antigravity | Observable Flutter #76&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/s5uGpolyHd4&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Watch as Craig Labenz is joined by Rody Davis and Andy Zhang to explore Flutter development in Google&apos;s new IDE, Antigravity.&lt;/p&gt;
&lt;p&gt;Download Antigravity → https://antigravity.google
GitHub → https://goo.gle/4nBfIQj&lt;/p&gt;
&lt;p&gt;Watch more Observable Flutter → https://goo.gle/ObservableFlutter
Subscribe to Flutter → http://goo.gle/FlutterYT&lt;/p&gt;
&lt;p&gt;#Flutter #ObservableFlutter&lt;/p&gt;
</content:encoded></item><item><title>How to build with Flutter and Google Antigravity</title><link>https://rodydavis.com/videos/antigravity/how-to-build-with-flutter-and-google-antigravity/</link><guid isPermaLink="true">https://rodydavis.com/videos/antigravity/how-to-build-with-flutter-and-google-antigravity/</guid><description>Learn how to build smooth, performant Flutter apps using Google&apos;s AntiGravity engine for enhanced motion and user experience.</description><pubDate>Wed, 19 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to build with Flutter and Google Antigravity&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/MpWMx-59PgI&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Google Antigravity: https://antigravity.google/
Flutter: https://flutter.dev&lt;/p&gt;
</content:encoded></item><item><title>Intro to Agent Skills</title><link>https://rodydavis.com/videos/antigravity/intro-to-agent-skills/</link><guid isPermaLink="true">https://rodydavis.com/videos/antigravity/intro-to-agent-skills/</guid><description>Intro to Agent Skills</description><pubDate>Sat, 28 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro to Agent Skills&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/4mnP1lRdUm8&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Introducing Agent Skills, a breakthrough method for giving your AI agent access to vast, specialized knowledge that only activates precisely when the task requires it. You’ll learn how to move beyond simple code completion by creating modular, on-demand skills that provide the deep context necessary for complex, modern development.&lt;/p&gt;
&lt;p&gt;Subscribe to Google Antigravity →    / @googleantigravity&lt;/p&gt;
&lt;p&gt;Google Antigravity is an agentic development platform, evolving the IDE into the agent-first era. Antigravity enables developers to operate at a higher, task-oriented level by managing agents across workspaces, while retaining a familiar AI IDE experience at its core. Agents operate across the editor, terminal, and browser, enabling them to autonomously plan and execute complex, end-to-end tasks elevating all aspects of software development.&lt;/p&gt;
&lt;p&gt;To learn more:
Website: https://antigravity.google/
X: https://x.com/antigravity
LinkedIn:   / google-antigravity&lt;/p&gt;
</content:encoded></item><item><title>Announcing TensorStore, Chrome Root Program, and more dev news!</title><link>https://rodydavis.com/videos/dev-show/announcing-tensorstore-chrome-root-program-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/dev-show/announcing-tensorstore-chrome-root-program-and-more-dev-news/</guid><description>Google Developer News Show: Learn about TensorStore for scalable array storage, the Chrome Root Program launch, a startup CPU boost for Cloud Run/Functions, and the Google Play Indie Games Fund in Latin America.</description><pubDate>Thu, 29 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Announcing TensorStore, Chrome Root Program, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/4n8oeHk9dLw&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 313 | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Introduction
0:10 - New startup CPU boost improves cold starts in Cloud Run, Cloud Functions → https://goo.gle/3xUAwej&lt;br&gt;
0:34 - TensorStore for High-Performance, Scalable Array Storage → https://goo.gle/3y0lLXo&lt;br&gt;
0:58 - Announcing the Launch of the Chrome Root Program →  https://goo.gle/3xZ3Hge&lt;br&gt;
1:16 - Google Play’s $2M Indie Games Fund in Latin America → https://goo.gle/3dNYzVg&lt;br&gt;
1:43 -Don’t forget to like, comment, and subscribe!&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody from Developer Relations. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow&lt;br&gt;
Subscribe to Google Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
</content:encoded></item><item><title>Blockchain-Based experiences, Govulncheck v1.0.0, and more dev news!</title><link>https://rodydavis.com/videos/dev-show/blockchain-based-experiences-govulncheck-v100-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/dev-show/blockchain-based-experiences-govulncheck-v100-and-more-dev-news/</guid><description>Google Developer News Show highlights new web platform features, Rust insights, the release of Govulncheck v1.0.0, and updates on blockchain experiences and Google Play policies.</description><pubDate>Mon, 17 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Blockchain-Based experiences, Govulncheck v1.0.0, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/HVdW-frdFGk&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 350 | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Intro
0:09 - New to the web platform in June → https://goo.gle/44whMQI
1:09 - Rust fact vs. fiction: 5 Insights from Google&apos;s Rust journey in 2022 →
https://goo.gle/471ZVTp
2:25 - Govulncheck v1.0.0 is released! → https://goo.gle/3OdoHIx
2:46 - Enabling New Blockchain-Based Experiences on Google Play  → https://goo.gle/44I9Tau
3:09 - New policy update to boost trust and transparency on Google Play → https://goo.gle/43rjuRG&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody, a Developer Advocate. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google for Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow
Subscribe to Google for Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
</content:encoded></item><item><title>Compression Streams support on all browsers, Android’s New Credential Manager, and more dev news!</title><link>https://rodydavis.com/videos/dev-show/compression-streams-support-on-all-browsers-androids-new-credential-manager-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/dev-show/compression-streams-support-on-all-browsers-androids-new-credential-manager-and-more-dev-news/</guid><description>Google Developer News Show: Learn about compression streams support, Android&apos;s new credential manager with passkeys, and developer workflow solutions.</description><pubDate>Tue, 07 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Compression Streams support on all browsers, Android’s New Credential Manager, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/RiVJzlTdD68&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 366  | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:08 -  Compression Streams support on all browsers → https://goo.gle/3QOMNLd
0:27 - Android with Credential Manager and passkeys → https://goo.gle/3Mu3hWv
0:57 - More ways for users to identify independently security tested apps on Google Play → https://goo.gle/4632HWD
1:25  - Developer solutions: Unlock and simplify your workflow → https://goo.gle/47ppAVp&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody Davis. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google for Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow
Subscribe to Google for Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
</content:encoded></item><item><title>Connect Google Services to Bard, Flutter’s Consulting Directory, and more developer news!</title><link>https://rodydavis.com/videos/dev-show/connect-google-services-to-bard-flutters-consulting-directory-and-more-developer-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/dev-show/connect-google-services-to-bard-flutters-consulting-directory-and-more-developer-news/</guid><description>Google Developer News Show: Bard updates, WASI support in Go, Flutter&apos;s consulting directory, and Space Invaders: World Defense development.</description><pubDate>Mon, 25 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Connect Google Services to Bard, Flutter’s Consulting Directory, and more developer news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/h-PG2r2e8Nk&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 360  | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Introduction
0:09 - Updates to Bard →https://goo.gle/48EOlOO
0:41 - WASI support in GO → https://goo.gle/3PSNItF
0:56 - Introducing Flutter’s Consulting Directory → https://goo.gle/452AIpr
1:19 - How we made Space Invaders: World Defense →https://goo.gle/48wxfmb&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody Davis, a Developer Relations Engineer. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google for Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow
Subscribe to Google for Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
</content:encoded></item><item><title>Dataset Search, Wear OS updates, and more dev news!</title><link>https://rodydavis.com/videos/dev-show/dataset-search-wear-os-updates-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/dev-show/dataset-search-wear-os-updates-and-more-dev-news/</guid><description>Google Developer News Show episode 331 covers datasets in Google Search, Firebase recap, Wear OS app quality requirements, and web platform updates.</description><pubDate>Mon, 06 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Dataset Search, Wear OS updates, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/Dg12-kjXMTk&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 331 | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Introduction
0:11 - Datasets at your fingertips in Google Search → https://goo.gle/3ydULmK
0:29 - Firebase 2022 Recap → https://goo.gle/3YrMhTT
0:54 - Policy Updates: New Wear OS App Quality Requirements → https://goo.gle/3ZmDFPC
1:14 - New to the web platform in February →  https://goo.gle/3ZNOt9v
1:37 - Don’t forget to like, comment, and subscribe!&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody, a Developer Advocate. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow&lt;br&gt;
Subscribe to Google Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers #web #data&lt;/p&gt;
</content:encoded></item><item><title>Jetpack Watch Face library, Datastream is GA, Bazel Con recap, and more dev news!</title><link>https://rodydavis.com/videos/dev-show/jetpack-watch-face-library-datastream-is-ga-bazel-con-recap-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/dev-show/jetpack-watch-face-library-datastream-is-ga-bazel-con-recap-and-more-dev-news/</guid><description>Google Developer News Show: Watch face development with Jeptack, Datasstream GA, Bazel Con recap, TensorFlow contributors, and more.</description><pubDate>Thu, 09 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Jetpack Watch Face library, Datastream is GA, Bazel Con recap, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/noK1Hnc4LrA&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 273 | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00- Introduction
0:11- Join Cloud Learn to build your Google Cloud skills → https://goo.gle/3y1M1zt
0:45- Develop Watch faces with Stable Jetpack Watch Face Library → https://goo.gle/31HIzh2
1:27- Datastream now Generally available → https://goo.gle/3IEZ0ft
1:58- Bazel Con 2021 Wrap Up → https://goo.gle/3rIklhO
2:34- Recognizing the 2021 TensorFlow Contributor Awardees → https://goo.gle/33d99zL
3:01-  Please remember to like, subscribe, and share!&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Developer Advocate Rody Davis. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below! 😃&lt;/p&gt;
&lt;p&gt;Follow Google Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow&lt;br&gt;
Subscribe to Google Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
</content:encoded></item><item><title>New to the web, try out Android predictive back gestures, and more dev news!</title><link>https://rodydavis.com/videos/dev-show/new-to-the-web-try-out-android-predictive-back-gestures-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/dev-show/new-to-the-web-try-out-android-predictive-back-gestures-and-more-dev-news/</guid><description>Google Developer News Show: Learn about Firebase Authentication updates, web platform features, predictive back gestures, Compose Text coloring, XNNPACK profiling, and more.</description><pubDate>Thu, 04 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;New to the web, try out Android predictive back gestures, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/6GFeGJ6jAy8&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 305 | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Introduction
0:11 - MFA, Blocking functions, and more come to Firebase Authentication → https://goo.gle/3BCrqoY
0:34 - New to the web platform in July → https://goo.gle/3Sk6ER8
0:56 - Prepare your app to support predictive back gestures → https://goo.gle/3bqZIAU
1:15 - Brushing up on Compose Text coloring → https://goo.gle/3By5iMv
1:36 - Profiling XNNPACK with TFLite → https://goo.gle/3zNVk8x
1:55- Please remember to like, subscribe, and share.&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody Davis from Developer Relations. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow&lt;br&gt;
Subscribe to Google Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
</content:encoded></item><item><title>Privacy Sandbox Demos, Pixel devices, and more dev news!</title><link>https://rodydavis.com/videos/dev-show/privacy-sandbox-demos-pixel-devices-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/dev-show/privacy-sandbox-demos-pixel-devices-and-more-dev-news/</guid><description>Google Developer News Show covering Pixel Fold/Tablet releases, Keyboard Lock API improvements, Privacy Sandbox demos, and Google Cloud VRP awards.</description><pubDate>Wed, 05 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Privacy Sandbox Demos, Pixel devices, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/FjZLZwOJySw&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 348  | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Introduction
0:11 - The new Pixel Fold &amp;amp; Pixel Tablet are here → https://goo.gle/3O2n5Be
0:53 - Better full screen mode with the Keyboard Lock API → https://goo.gle/3PJAPCb
1:23 - Introducing Privacy Sandbox Demos → https://goo.gle/3NDqF3x
1:54 - Google Cloud Awards $313,337 in 2022 VRP Prizes → https://goo.gle/3rdoyvU
2:21 - Don’t forget to like, comment, and subscribe!&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody, a Developer Advocate. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google for Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow
Subscribe to Google for Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google for #Developers&lt;/p&gt;
</content:encoded></item><item><title>Scaling vision transformers, Compose Pager, and more dev news!</title><link>https://rodydavis.com/videos/dev-show/scaling-vision-transformers-compose-pager-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/dev-show/scaling-vision-transformers-compose-pager-and-more-dev-news/</guid><description>Google Developer News Show: Learn about scaling vision transformers, Compose Pager customization, WebAssembly advancements, and more developer updates.</description><pubDate>Mon, 10 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Scaling vision transformers, Compose Pager, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/wwx-NH4LwrQ&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 336  | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Introduction
0:10 - Wonderous nominated for Webby Award → https://goo.gle/40LIliK
0:28 - How WebAssembly is accelerating new web functionality →https://goo.gle/3ZR0L04
0:54 - Scaling vision transformers to 22 billion parameters →https://goo.gle/3mjc7fG
1:29 - Customizing Compose Pager with fun indicators and transitions →https://goo.gle/43dVOS9
1:48 -  Don’t forget to like, comment, and subscribe!&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody, a Developer Advocate. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow&lt;br&gt;
Subscribe to Google Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
</content:encoded></item><item><title>Build full-stack AI apps in minutes with Firebase Studio</title><link>https://rodydavis.com/videos/firebase-studio/build-full-stack-ai-apps-in-minutes-with-firebase-studio/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/build-full-stack-ai-apps-in-minutes-with-firebase-studio/</guid><description>Learn how to accelerate full-stack app development with Firebase Studio, leveraging AI tools like Gemini for code generation and faster shipping, as presented at Google I/O 2025.</description><pubDate>Thu, 22 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Build full-stack AI apps in minutes with Firebase Studio&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/x2zvki_VlRE&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The way we build software is fundamentally changing with AI-centric tools. This session covers how Project IDX helps you build and ship full-stack apps faster. Your codebase becomes context for Gemini, enabling powerful agentic capabilities in your editor. Generate and modify code or execute commands, all with natural language prompts. You’ll leave this session with strategies to ship better software, quicker.&lt;/p&gt;
&lt;p&gt;Speakers: Kirupa Chinnathambi, Rody Davis&lt;/p&gt;
&lt;p&gt;Check out the cloud session track from Google I/O 2025 → https://goo.gle/io25-gct-yt
Check out out all the Firebase sessions from Google I/O 2025→ https://goo.gle/io25-firebase-yt
Check out all sessions from Google I/O 2025→ https://goo.gle/io25-sessions-yt&lt;/p&gt;
&lt;p&gt;Subscribe to Firebase → https://goo.gle/Firebase&lt;/p&gt;
&lt;p&gt;Event: Google I/O 2025&lt;/p&gt;
&lt;p&gt;Products Mentioned: Cloud&lt;/p&gt;
</content:encoded></item><item><title>Building AI Powered Experiences with Firebase Studio and Firebase Platform - Rody Davis Building AI Powered Experiences with Firebase Studio and Firebase Platform - Rody Davis</title><link>https://rodydavis.com/videos/firebase-studio/building-ai-powered-experiences-with-firebase-studio-and-firebase-platform-rody-davis/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/building-ai-powered-experiences-with-firebase-studio-and-firebase-platform-rody-davis/</guid><description>Learn how to rapidly build AI-powered applications with Firebase Studio and the Gemini API in this demo-driven guide by Firebase Developer Advocate Rody Davis.</description><pubDate>Mon, 29 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building AI Powered Experiences with Firebase Studio and Firebase Platform - Rody Davis&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/DqCWNREk7c4&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The demand for AI-powered features in applications is exploding, but integrating generative AI is often complex and slow. This session introduces Firebase Studio, a new AI powered Cloud Based Workspace on the Firebase platform designed to dramatically simplify building AI backends.&lt;/p&gt;
&lt;p&gt;Join us for a demo-driven guide on leveraging the Gemini API to build sophisticated features with remarkable speed. You&apos;ll leave with a clear understanding of how to use the Firebase ecosystem to rapidly prototype and ship scalable, secure, and intelligent applications&lt;/p&gt;
&lt;p&gt;Sobre Rody Davis: Developer Advocate for Firebase Studio at Google. Working closely with Flutter, Angular, Firebase, Go and Gemini in Google Cloud. In my time at Google I have also supported Material Design, Lit (previously Polymer) and Google Developers (Solutions).&lt;/p&gt;
&lt;p&gt;I have a background in systems administration, audio engineering, music and electrical engineering. I am very passionate about open source and creative applications of software. Currently I am working with teams to take advantage of AI in software development and integration into apps.&lt;/p&gt;
&lt;p&gt;My mission is to make the intersection of Google products work better together.&lt;/p&gt;
&lt;p&gt;🛤️ Track: DEVELOPMENT
📋 Formato: Charla&lt;/p&gt;
</content:encoded></item><item><title>Building Firebase Studio Rules for Angular Live</title><link>https://rodydavis.com/videos/firebase-studio/building-firebase-studio-rules-for-angular-live/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/building-firebase-studio-rules-for-angular-live/</guid><description>Learn how to create Firebase Studio rules using AI to streamline Angular app development in the Firebase Studio IDE.</description><pubDate>Sat, 14 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building Firebase Studio Rules for Angular Live&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/YfdsFSVyZ4I&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Join in as Mark and Rody build rules in AI Studio to help developers build Angular Apps in the Firebase Studio IDE.&lt;/p&gt;
&lt;p&gt;Try Firebase Studio: goo.gle/4e8748C&lt;/p&gt;
</content:encoded></item><item><title>Compile SQLite from source to WASM in Firebase Studio</title><link>https://rodydavis.com/videos/firebase-studio/compile-sqlite-from-source-to-wasm-in-firebase-studio/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/compile-sqlite-from-source-to-wasm-in-firebase-studio/</guid><description>Learn how to compile SQLite to WASM in Firebase Studio using Nix, web preview, and Gemini for a streamlined development workflow.</description><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Compile SQLite from source to WASM in Firebase Studio&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/hox-Sv8ZHxE&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Rody, a Developer Relations Engineer for Firebase Studio shares how easy it is to get started with a blank template in Firebase Studio. In this video, he shows how to clone your code inside a workstation and use nix to install dependencies. Watch a step by step tutorial on cloning code, setting dev.nix, web preview, and leverage Gemini in Firebase Studio.&lt;/p&gt;
&lt;p&gt;Chapters:
0:00 - Get started
1:16 - Cloning SQLite and Emscripten
1:42 - Set up the VM
3:47 - Configure web preview
5:15 - Leverage Gemini
5:47 - Recap&lt;/p&gt;
&lt;p&gt;Firebase Studio → https://goo.gle/firebasestudio&lt;/p&gt;
&lt;p&gt;#Firebase #SQLite #WASM #NixOS&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned:,  Firebase,&lt;/p&gt;
</content:encoded></item><item><title>Explore all the official templates for Firebase Studio</title><link>https://rodydavis.com/videos/firebase-studio/explore-all-the-official-templates-for-firebase-studio/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/explore-all-the-official-templates-for-firebase-studio/</guid><description>Learn how to use official templates for Firebase Studio, supporting popular frameworks and languages, to accelerate your development process.</description><pubDate>Fri, 09 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Explore all the official templates for Firebase Studio&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/vWfoxcDETY0&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Check out and learn how to use one of our many templates for Firebase Studio covering lots of popular frameworks and languages.&lt;/p&gt;
&lt;p&gt;Firebase Studio → https://goo.gle/firebasestudio&lt;/p&gt;
&lt;p&gt;#Firebase&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned: Firebase,&lt;/p&gt;
</content:encoded></item><item><title>Firebase After Hours #14: Live Vibe Coding with Firebase Studio</title><link>https://rodydavis.com/videos/firebase-studio/firebase-after-hours-14-live-vibe-coding-with-firebase-studio/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/firebase-after-hours-14-live-vibe-coding-with-firebase-studio/</guid><description>Watch a live coding session with Firebase Studio, featuring developers discussing and answering questions about the tool, hosted by Firebase and Firebase Studio experts.</description><pubDate>Thu, 31 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Firebase After Hours #14: Live Vibe Coding with Firebase Studio&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/wbrI2egh7bQ&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Join @PeterFriese and @NoheDev  on Firebase After Hours as they vibe code an app (or two) using Firebase Studio. @RodyDavis (Developer Relations Engineer on Firebase Studio) will be their guest. Bring your questions about Firebase Studio, and they&apos;ll answer them live!&lt;/p&gt;
&lt;p&gt;Resources:
From vibe to reality: Integrating Firebase in Firebase Studio → https://goo.gle/4lWAXf8
Forum: → https://community.firebasestudio.dev/&lt;/p&gt;
&lt;p&gt;#Firebase #FirebaseAfterHours #FirebaseStudio #Streaming #GoogleCloud #LiveStream #DeveloperCommunity&lt;/p&gt;
</content:encoded></item><item><title>Firebase Studio now supports Gemma 3 image inputs on the Gemini Developer API 🔥🪄🚀</title><link>https://rodydavis.com/videos/firebase-studio/firebase-studio-now-supports-gemma-3-image-inputs-on-the-gemini-developer-api-/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/firebase-studio-now-supports-gemma-3-image-inputs-on-the-gemini-developer-api-/</guid><description>Firebase Studio now integrates with the Gemini Developer API, enabling image inputs using Gemma 3 models, accessible through a dedicated template.</description><pubDate>Fri, 09 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Firebase Studio now supports Gemma 3 image inputs on the Gemini Developer API 🔥🪄🚀&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/tndZqTcQWK4&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Try it out on the Gemini template and switch to the gemma-27b-it model!
https://studio.firebase.google.com/new/gemini&lt;/p&gt;
&lt;p&gt;Learn more about Gemma 3:
https://ai.google.dev/gemma/docs/core&lt;/p&gt;
&lt;p&gt;Website: https://rodydavis.com
Twitter: @rodydavis
Github: @rodydavis
Patreon: https://www.patreon.com/rodydavis
Buy Me A Coffee: https://www.buymeacoffee.com/3mgFNYd&lt;/p&gt;
</content:encoded></item><item><title>From prototype to IDE in Firebase Studio</title><link>https://rodydavis.com/videos/firebase-studio/from-prototype-to-ide-in-firebase-studio/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/from-prototype-to-ide-in-firebase-studio/</guid><description>Learn how to customize code and use Gemini AI within the Firebase Studio IDE to streamline development workflows.</description><pubDate>Thu, 08 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;From prototype to IDE in Firebase Studio&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/9CLNxcM1tbo&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Take advantage of Firebase Studios IDE and VM features! Rody, a Developer Relations Engineer for Firebase Studio shares how you can customize code and use Gemini to make edits.&lt;/p&gt;
&lt;p&gt;Firebase Studio → https://goo.gle/firebasestudio&lt;/p&gt;
&lt;p&gt;#Firebase #NixOS&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned:,  Firebase,&lt;/p&gt;
</content:encoded></item><item><title>How Firebase Studio accelerates development</title><link>https://rodydavis.com/videos/firebase-studio/how-firebase-studio-accelerates-development/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/how-firebase-studio-accelerates-development/</guid><description>Firebase Studio is a new tool that accelerates interactive prototyping with Next.js and Gankit, offering AI-powered blueprints, fast customization, and one-click deployment for quickly building Firebase projects and prototypes.</description><pubDate>Tue, 29 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How Firebase Studio accelerates development&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/llByU3YZvew&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Get an exciting first look at Firebase Studio—a powerful new tool designed for interactive prototyping with Next.js and Genkit. In this video, Rody from the Firebase team shows just how quickly you can kickstart a project, using AI to instantly generate a blueprint for a tipping calculator app. With fast customization, one-click deployment, and auto-provisioning of Firebase projects and Gemini API keys, it’s never been easier to go from idea to working prototype.&lt;/p&gt;
&lt;p&gt;Try it for yourself → https://studio.firebase.google.com&lt;/p&gt;
&lt;p&gt;#GoogleCloudNext #Firebase&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned: Firebase Studio&lt;/p&gt;
</content:encoded></item><item><title>Import existing projects into Firebase Studio</title><link>https://rodydavis.com/videos/firebase-studio/import-existing-projects-into-firebase-studio/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/import-existing-projects-into-firebase-studio/</guid><description>Learn how to import existing projects from GitHub, GitLab, and other sources into Firebase Studio for streamlined development.</description><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Import existing projects into Firebase Studio&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/31GsuvcRcXk&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Get started with your own codebase in Firebase Studio. Rody, a Developer Relations Engineer for Firebase Studio shares how you can import your project from a variety of sources such as Github, GitLab, and more!&lt;/p&gt;
&lt;p&gt;Firebase Studio → https://goo.gle/firebasestudio&lt;/p&gt;
&lt;p&gt;#Firebase #Git&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned:, Firebase&lt;/p&gt;
</content:encoded></item><item><title>Introducing Firebase Studio</title><link>https://rodydavis.com/videos/firebase-studio/introducing-firebase-studio/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/introducing-firebase-studio/</guid><description>Firebase Studio is an AI-powered development environment that accelerates the creation and deployment of full-stack AI applications, from APIs to mobile, enabling faster development workflows.</description><pubDate>Fri, 30 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Introducing Firebase Studio&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/vVAui3_rvD8&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Discover Firebase Studio, an AI-powered agentive development environment that helps developers build and ship production quality full stack AI apps. From APIs to backends, frontends, mobile, and more, Firebase Studio is the tool you need to bring your ideas to life faster than ever.&lt;/p&gt;
&lt;p&gt;Chapters:
0:00 - Firebase Studio overview
0:30 - Prototyping with AI
2:08 - Code mode and customization
2:36 - Preview and deploy
3:06 - Recap&lt;/p&gt;
&lt;p&gt;Resources:
Check out the docs → https://goo.gle/4micjGg&lt;/p&gt;
&lt;p&gt;Subscribe to Firebase → https://goo.gle/Firebase&lt;/p&gt;
&lt;p&gt;#Firebase&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned: Firebase,&lt;/p&gt;
</content:encoded></item><item><title>Start prototyping with Firebase Studio</title><link>https://rodydavis.com/videos/firebase-studio/start-prototyping-with-firebase-studio/</link><guid isPermaLink="true">https://rodydavis.com/videos/firebase-studio/start-prototyping-with-firebase-studio/</guid><description>Learn how to prototype and build apps quickly with Firebase Studio, a tool for rapid app development and deployment.</description><pubDate>Wed, 07 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Start prototyping with Firebase Studio&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/hB1BdhINRNE&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Did you know you can build an app with Firebase Studio? Rody, a Developer Relations Engineer for Firebase Studio shares how to build and deploy an app using the prototype feature.&lt;/p&gt;
&lt;p&gt;Firebase Studio → https://studio.firebase.google.com&lt;/p&gt;
&lt;p&gt;#Firebase  #VibeCoding #AI&lt;/p&gt;
&lt;p&gt;Speaker: Rody Davis
Products Mentioned:,  Firebase&lt;/p&gt;
</content:encoded></item><item><title>Create simple, beautiful UI with Flutter</title><link>https://rodydavis.com/videos/flutter/create-simple-beautiful-ui-with-flutter/</link><guid isPermaLink="true">https://rodydavis.com/videos/flutter/create-simple-beautiful-ui-with-flutter/</guid><description>Learn how to create beautiful and engaging user interfaces with Flutter using simple, incremental design techniques in this Google I/O 2022 workshop.</description><pubDate>Wed, 11 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Create simple, beautiful UI with Flutter&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/eBVMfS7MayI&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Beautiful experiences and great UI don&apos;t happen by themselves, but that doesn&apos;t mean they have to be difficult! Watch two design experts apply simple, straightforward, incremental techniques to take a well-built but boring-looking app and turn it into something memorable.&lt;/p&gt;
&lt;p&gt;Resources:
Codelab → https://goo.gle/3Mn16SF&lt;/p&gt;
&lt;p&gt;Speakers: Khanh Nguyen, Rody Davis&lt;/p&gt;
&lt;p&gt;Watch more:
All Google I/O 2022 Sessions → https://goo.gle/IO22_AllSessions
Flutter at I/O 2022 playlist → https://goo.gle/IO22_Flutter
All Google I/O 2022 workshops → https://goo.gle/IO22_Workshops&lt;/p&gt;
&lt;p&gt;Subscribe to Flutter! → https://goo.gle/FlutterYT&lt;/p&gt;
&lt;p&gt;#GoogleIO&lt;/p&gt;
</content:encoded></item><item><title>Flutter as a Service (Webinar)</title><link>https://rodydavis.com/videos/flutter/flutter-as-a-service-webinar/</link><guid isPermaLink="true">https://rodydavis.com/videos/flutter/flutter-as-a-service-webinar/</guid><description>Learn how to monetize your Flutter development skills in this webinar featuring Rody Davis and Frederik Schweiger.</description><pubDate>Tue, 22 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter as a Service (Webinar)&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/b8ftugJvgGY&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Rody Davis and Frederik Schweiger speak as part of an online webinar for making money with Flutter as a service.&lt;/p&gt;
</content:encoded></item><item><title>Flutter Plugin - Live Coding - Native Color Picker</title><link>https://rodydavis.com/videos/flutter/flutter-plugin-live-coding-native-color-picker/</link><guid isPermaLink="true">https://rodydavis.com/videos/flutter/flutter-plugin-live-coding-native-color-picker/</guid><description>Flutter plugin for a native color picker, demonstrating the development process through debugging and code creation.</description><pubDate>Wed, 01 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Flutter Plugin - Live Coding - Native Color Picker&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/zY3mWKifRd8&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;In this video I walk through my debugging and plugin creation process!&lt;/p&gt;
&lt;p&gt;Source: https://github.com/rodydavis/native_color_picker&lt;/p&gt;
</content:encoded></item><item><title>IDX &amp; Flutter by Rody Davis, Senior Developer Advocate - Google</title><link>https://rodydavis.com/videos/flutter/idx-flutter-by-rody-davis-senior-developer-advocate-google/</link><guid isPermaLink="true">https://rodydavis.com/videos/flutter/idx-flutter-by-rody-davis-senior-developer-advocate-google/</guid><description>Google&apos;s Rody Davis discusses how IDX simplifies developer workflows, particularly when used with Flutter and Dart.</description><pubDate>Tue, 29 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;IDX &amp;amp; Flutter by Rody Davis, Senior Developer Advocate - Google&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/jhNzq-zF98E&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;@RodyDavis  spoke about IDX and Flutter and demonstrated how much easier life becomes for a developer when using IDX&lt;/p&gt;
&lt;p&gt;#flutter #idx #dart #developer #google #fluttersiliconvalley&lt;/p&gt;
</content:encoded></item><item><title>Introducing the signals_hooks package for Flutter hooks and signals</title><link>https://rodydavis.com/videos/flutter/introducing-the-signals_hooks-package-for-flutter-hooks-and-signals/</link><guid isPermaLink="true">https://rodydavis.com/videos/flutter/introducing-the-signals_hooks-package-for-flutter-hooks-and-signals/</guid><description>`signals_hooks` is a Flutter package providing convenient hooks for managing signals, simplifying state management and reactive programming in Flutter applications.</description><pubDate>Sat, 01 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Introducing the signals_hooks package for Flutter hooks and signals&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/732LqgfYbDQ&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Website: https://rodydavis.com
Twitter: @rodydavis
BlueSky: @rodydavis.com
Github: @rodydavis
Patreon: https://www.patreon.com/rodydavis
Buy Me A Coffee: https://www.buymeacoffee.com/3mgFNYd&lt;/p&gt;
</content:encoded></item><item><title>Material 3 from design to deployment</title><link>https://rodydavis.com/videos/flutter/material-3-from-design-to-deployment/</link><guid isPermaLink="true">https://rodydavis.com/videos/flutter/material-3-from-design-to-deployment/</guid><description>Learn to implement Material 3 design in Flutter with advanced theming for multi-device support in this video tutorial.</description><pubDate>Wed, 25 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Material 3 from design to deployment&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/7nrhTdS7dHg&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Learn how to build the basil material study in Flutter with advanced theming techniques that support multiple device contexts.&lt;/p&gt;
&lt;p&gt;Speakers: Rody Davis, Liam Spradlin&lt;/p&gt;
&lt;p&gt;Watch more:
Watch all the sessions → https://goo.gle/FlutterForwardYT&lt;/p&gt;
&lt;p&gt;Subscribe to Flutter → http://goo.gle/FlutterYT&lt;/p&gt;
&lt;p&gt;#FlutterForward&lt;/p&gt;
</content:encoded></item><item><title>Material You: Applying dynamic color to your app and brand</title><link>https://rodydavis.com/videos/flutter/material-you-applying-dynamic-color-to-your-app-and-brand/</link><guid isPermaLink="true">https://rodydavis.com/videos/flutter/material-you-applying-dynamic-color-to-your-app-and-brand/</guid><description>Learn how to implement Material You&apos;s dynamic color theming in your Android app with the latest tools and resources, including the Material Theme Builder and M3 Design Kit.</description><pubDate>Wed, 27 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Material You: Applying dynamic color to your app and brand&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/4bguZJwHqsQ&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Material You is enabling a new level of individuality across interfaces. But how does dynamic color interact with the distinct brand expression in your app? We dive into the NEW tooling for visualizing dynamic color and seeing those changes reflected throughout your app&apos;s UI.&lt;/p&gt;
&lt;p&gt;Resources:
Material Theme Builder → https://goo.gle/material-theme-builder-web
Material Theme builder available for Figma : https://goo.gle/material-theme-builder-figma
M3 Design Kit : https://goo.gle/m3-design-kit&lt;/p&gt;
&lt;p&gt;Speakers:
Ivy Knight, Rody Davis&lt;/p&gt;
&lt;p&gt;Watch more:
Watch all the Android Dev Summit sessions → https://goo.gle/ads21-allsessions
Watch all the Jetpack Compose, now with Material You sessions → https://goo.gle/ads21-materialcompose&lt;/p&gt;
&lt;p&gt;Subscribe to Android Developers → https://goo.gle/AndroidDevs&lt;/p&gt;
&lt;p&gt;#AndroidDevSummit #MaterialYou #Featured #Latest&lt;/p&gt;
&lt;p&gt;product: Material Design - Material You; event: Android Dev Summit 2021; fullname: Ivy Knight, Rody Davis; re_ty: Publish;&lt;/p&gt;
</content:encoded></item><item><title>Rody Davis: Building Adaptive UI/UX in Flutter</title><link>https://rodydavis.com/videos/flutter/rody-davis-building-adaptive-uiux-in-flutter/</link><guid isPermaLink="true">https://rodydavis.com/videos/flutter/rody-davis-building-adaptive-uiux-in-flutter/</guid><description>Learn how to build adaptive UI/UX designs for Flutter applications that seamlessly work across various devices and platforms with these practical patterns and examples.</description><pubDate>Wed, 13 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Rody Davis: Building Adaptive UI/UX in Flutter&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/P1B52fRGjbE&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;When developing with a cross platform framework there are new problems you are faced with that you do not get when designing for native or web. When we build UI we need to create adaptive experiences that go from a watch, passive modes on a TV or voice only, mobile devices, desktops and everything in between. It takes extra work but if you follow these patterns you will save a lot of time later.&lt;/p&gt;
&lt;p&gt;Rody&apos;s links:
Adaptive UI Demo - https://rodydavis.github.io/adaptive_ui
YouTube - https://www.youtube.com/rodydavis
GitHub - https://github.com/rodydavis
Twitter - https://www.twitter.com/rodydavis
LinkedIn - https://www.linkedin.com/in/rodydavis
Medium - https://medium.com/@rody.davis.jr
pub.dev - https://pub.dev/publishers/rodydavis.com/packages
Creative Engineering podcast: https://rodydavis.github.io/creative_engineering/
https://www.rodydavis.com&lt;/p&gt;
</content:encoded></item><item><title>60 - Flutter with Rody Davis</title><link>https://rodydavis.com/videos/interviews/60-flutter-with-rody-davis/</link><guid isPermaLink="true">https://rodydavis.com/videos/interviews/60-flutter-with-rody-davis/</guid><description>Flutter development interview with Rody Davis covering the cross-platform framework and Dropbox&apos;s mobile development challenges.</description><pubDate>Mon, 07 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;60 - Flutter with Rody Davis&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/gcU80lHHslI&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Alex interviews Rody Davis about Flutter, a cross platform framework for iOS and Android development. They also address cross platform development issues Dropbox experienced with their mobile apps.&lt;/p&gt;
</content:encoded></item><item><title>Chat With Lit #1 - Westbrook Johnson (Adobe)</title><link>https://rodydavis.com/videos/interviews/chat-with-lit-1-westbrook-johnson-adobe/</link><guid isPermaLink="true">https://rodydavis.com/videos/interviews/chat-with-lit-1-westbrook-johnson-adobe/</guid><description>Adobe&apos;s Westbrook Johnson discusses topics with the Lit team in this archived Twitter Space recording hosted by Rody Davis and Elliott Marquez.</description><pubDate>Sat, 24 Jul 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Chat With Lit #1 - Westbrook Johnson (Adobe)&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/it-NXhxkOJo&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Chat With Lit is the Lit team&apos;s live-recorded Twitter Space series hosted by Rody Davis (@rodydavis) and Elliott Marquez (@techytacos) from Google.&lt;/p&gt;
&lt;p&gt;This is the first Twitter Space featuring Westbrook Johnson (@WestbrookJ) from Adobe.&lt;/p&gt;
&lt;p&gt;Follow the Lit Twitter account at https://twitter.com/buildWithLit to join in live at the next Space!&lt;/p&gt;
</content:encoded></item><item><title>Chatting with a Googler @RodyDavis ! #google #flutter #firebase #developers #ai #vibecoding Chatting with a Googler @RodyDavis ! #google #flutter #firebase #developers #ai #vibecoding</title><link>https://rodydavis.com/videos/interviews/chatting-with-a-googler-rodydavis-google-flutter-firebase-developers-ai-vibecoding/</link><guid isPermaLink="true">https://rodydavis.com/videos/interviews/chatting-with-a-googler-rodydavis-google-flutter-firebase-developers-ai-vibecoding/</guid><description>Google Developer Group interview with Firebase Studio DevRel Rody Davis discusses new Gemini AI models, vibe coding, and building apps with Flutter and Firebase Studio, plus his predictions for the future of AI and web technologies.</description><pubDate>Mon, 07 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Chatting with a Googler @RodyDavis ! #google #flutter #firebase #developers #ai #vibecoding&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/v63OXnO5Qqs&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;As GDG organizers, ​⁠​⁠@GoogleDevelopers and ​⁠​⁠@Google communities organised a GDG EMEA summit 2025. During this time, ​⁠​⁠@shrinishdonde343 had the pleasure to interview ​⁠​⁠@RodyDavis, a DevRel for Firebase Studio at Google. The talk focused on new AI models in Gemini such as Gemini 2.5 Pro, Gemini 2.5 Flash etc. It also highlighted some trendy topics such as vibe coding and prompting and focused on how to best use tools such as Firebase studio to build exciting Apps.&lt;/p&gt;
&lt;p&gt;Rody also walks us through how easy it is getting as the time progresses to build an app using ​⁠​⁠@Firebase studio and ​⁠​⁠@flutterdev . Stay tuned until the end to hear his views about where the future of AI and app and web technologies will be in the next few years. Also do follow ​⁠​⁠@gdgberlin.&lt;/p&gt;
&lt;p&gt;Learn more about Google for Developers ​⁠​⁠@GoogleDevelopers here: https://goo.gle/40yMI2w&lt;/p&gt;
&lt;p&gt;Special thanks to ​⁠​⁠@alicjaheisig-chiarello1986  for helping us during the process and to the crew members for their wonderful support.&lt;/p&gt;
</content:encoded></item><item><title>Interview with Rody Davis - Firebase</title><link>https://rodydavis.com/videos/interviews/interview-with-rody-davis-firebase/</link><guid isPermaLink="true">https://rodydavis.com/videos/interviews/interview-with-rody-davis-firebase/</guid><description>Google Developer Advocate Rody Davis discusses Firebase Studio, Flutter, Angular, AI in software development, and the integration of Google products.</description><pubDate>Tue, 21 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Interview with Rody Davis - Firebase&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/D2q_y1GFAIo&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Rody Davis is a Developer Advocate for Firebase Studio at Google. Working closely with Flutter, Angular, Firebase, Go and Gemini in Google Cloud. In my time at Google I have also supported Material Design, Lit (previously Polymer) and Google Developers (Solutions). I have a background in systems administration, audio engineering, music and electrical engineering. I am very passionate about open source and creative applications of software. Currently I am working with teams to take advantage of AI in software development and integration into apps. My mission is to make the intersection of Google products work better together.&lt;/p&gt;
</content:encoded></item><item><title>Speaker Interview - Rody Davis, David Khourshid, and João Luiz S Kestering</title><link>https://rodydavis.com/videos/interviews/speaker-interview-rody-davis-david-khourshid-and-joo-luiz-s-kestering/</link><guid isPermaLink="true">https://rodydavis.com/videos/interviews/speaker-interview-rody-davis-david-khourshid-and-joo-luiz-s-kestering/</guid><description>ThunderNerds interview with mobile developers Rody Davis, David Khourshid, and João Luiz S. Kestering at Devfest Florida, covering Flutter, open-source, and front-end technologies.</description><pubDate>Wed, 20 Nov 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Speaker Interview - Rody Davis, David Khourshid, and João Luiz S Kestering&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/RuVyxtZZbhY&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;The ThunderNerds interviewing our speakers at Devfest Florida.&lt;/p&gt;
&lt;h3&gt;Rody Davis&lt;/h3&gt;
&lt;p&gt;Full stack mobile developer from Alabama and currently in Tampa for the last 7 years. I have created over 12 apps with on iOS and Android released for production. I love VR, Game Development, Music production and graphic design. I have been using Flutter since beta 2. I contribute a lot to open source and love showing others how tech can make our lives better and how great UX can make us love the products we use.&lt;/p&gt;
&lt;h3&gt;David Khourshid&lt;/h3&gt;
&lt;p&gt;David Khourshid is a software engineer for Microsoft, a tech author, and speaker. Also a fervent open-source contributor, he is passionate about statecharts and software modeling, reactive animations, innovative user interfaces, and cutting-edge front-end technologies. When not behind a computer keyboard, he’s behind a piano keyboard or traveling.&lt;/p&gt;
&lt;h3&gt;João Luiz S. Kestering&lt;/h3&gt;
&lt;p&gt;I&apos;m software engineer on Modus Create with post-degree in Software Architecture with focus on frontend applications using Angular and React. Now started a new adventure using Flutter. On my free time, I like to watch football, read and play video games.&lt;/p&gt;
</content:encoded></item><item><title>Take 5 - Adaptive Dialogs - Flutter</title><link>https://rodydavis.com/videos/take-5/adaptive-dialogs-flutter/</link><guid isPermaLink="true">https://rodydavis.com/videos/take-5/adaptive-dialogs-flutter/</guid><description>Build responsive dialogs in Flutter that adapt to different screen sizes (desktop, mobile, web) using the Take 5 method.</description><pubDate>Tue, 21 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Take 5 - Adaptive Dialogs - Flutter&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/AQybpww4MgQ&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Source Code: https://github.com/rodydavis/flutter_take_5&lt;/p&gt;
&lt;p&gt;In this video I walk though how to build an adaptive dialog that looks good on desktop, mobile and web. Whenever you launch a dialog you are not guaranteed that the size is constant. In this example we will show you how to fix those edge cases.&lt;/p&gt;
</content:encoded></item><item><title>Take 5 - Internationalization - Flutter</title><link>https://rodydavis.com/videos/take-5/internationalization-flutter/</link><guid isPermaLink="true">https://rodydavis.com/videos/take-5/internationalization-flutter/</guid><description>Learn how to easily internationalize your Flutter app using VSCode and Google Translate to generate translations for multiple languages.</description><pubDate>Tue, 28 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Take 5 - Internationalization - Flutter&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/EY7tF7ryLVw&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Source Code: https://github.com/rodydavis/flutter_take_5&lt;/p&gt;
&lt;p&gt;In this video I show you how easy it is to internationalize your Flutter application. You can use VSCode to generate the code fo you, and have Google Translate create the other languages based off of your default locale.&lt;/p&gt;
</content:encoded></item><item><title>Take 5 - Master Detail Scaffold - Flutter</title><link>https://rodydavis.com/videos/take-5/master-detail-scaffold-flutter/</link><guid isPermaLink="true">https://rodydavis.com/videos/take-5/master-detail-scaffold-flutter/</guid><description>Flutter tutorial demonstrating how to build a responsive Master-Detail list view that works seamlessly across desktop, mobile, and tablet devices, similar to UIKit&apos;s Master-Detail Controller.</description><pubDate>Mon, 13 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Take 5 - Master Detail Scaffold - Flutter&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/1i73KbI2Uhg&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Source Code: https://github.com/rodydavis/flutter_take_5&lt;/p&gt;
&lt;p&gt;In this video I walk though how to build a ListView that works great on desktop, mobile and tablets. Similar to UIKit Master Detail Controller we explore how to achieve a similar effect in Flutter.&lt;/p&gt;
</content:encoded></item><item><title>Take 5 - Responsive Design - Flutter</title><link>https://rodydavis.com/videos/take-5/responsive-design-flutter/</link><guid isPermaLink="true">https://rodydavis.com/videos/take-5/responsive-design-flutter/</guid><description>Learn how to build a responsive Flutter app with adaptive scaffolds and reusable widgets, using a technique to define layout behavior based on breakpoints, and explore a package for simplifying responsive navigation.</description><pubDate>Wed, 08 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Take 5 - Responsive Design - Flutter&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/kGhkiKp939c&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Source Code: https://github.com/rodydavis/flutter_take_5&lt;/p&gt;
&lt;p&gt;In this video I walk through how to build a flutter app with an adaptive scaffold. This is using a technique to have each widget define layout behavior based on breakpoints. This is very useful for having total reuse over all your screens. Responsive layout doesn&apos;t mean you need to have duplicate code.&lt;/p&gt;
&lt;p&gt;I also mention the package I created to abstract this for you and it can be found on pub here:
https://pub.dev/packages/navigation_rail&lt;/p&gt;
&lt;p&gt;In the upcoming videos I will go more in depth on how to adapt the individual screens. This video is mostly about how to do the main navigation in a responsive way.&lt;/p&gt;
</content:encoded></item><item><title>Take 5 - Shared Preferences - Flutter</title><link>https://rodydavis.com/videos/take-5/shared-preferences-flutter/</link><guid isPermaLink="true">https://rodydavis.com/videos/take-5/shared-preferences-flutter/</guid><description>Learn how to use Shared Preferences in Flutter to implement dark mode and check for fresh app installations, with code examples available on GitHub.</description><pubDate>Mon, 04 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Take 5 - Shared Preferences - Flutter&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/U2jdTCMBTgE&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Source Code: https://github.com/rodydavis/flutter_take_5&lt;/p&gt;
&lt;p&gt;In this video I talk about how to get started with Shared Preferences in Flutter. We will setup dark mode, and a fresh install check for our application.&lt;/p&gt;
</content:encoded></item><item><title>Take 5 - Your First Flutter Project</title><link>https://rodydavis.com/videos/take-5/your-first-flutter-project/</link><guid isPermaLink="true">https://rodydavis.com/videos/take-5/your-first-flutter-project/</guid><description>Learn how to create your first Flutter project with this tutorial, including code and resources.</description><pubDate>Sat, 29 Feb 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Take 5 - Your First Flutter Project&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/rtBkU4pvHcw&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Blog Post: https://medium.com/@rody.davis.jr/creating-your-first-flutter-project-58fd0312204b
Source Code: https://github.com/rodydavis/flutter_take_5/&lt;/p&gt;
</content:encoded></item><item><title>5 things you can do to prepare your app for large screens | Session</title><link>https://rodydavis.com/videos/talks/5-things-you-can-do-to-prepare-your-app-for-large-screens-session/</link><guid isPermaLink="true">https://rodydavis.com/videos/talks/5-things-you-can-do-to-prepare-your-app-for-large-screens-session/</guid><description>Learn practical strategies using Material Design to adapt your Android app for large screens with limited resources in this Google I/O 2021 session.</description><pubDate>Wed, 19 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;5 things you can do to prepare your app for large screens | Session&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/UNDZn9GKJGo&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;In this Session, you learn how Material Design can help you get your app ready for large devices when time and resources are constrained. From prioritizing design characteristics to implementation details, this Session will give developers and designers a picture of what’s possible without undergoing a complete redesign or a comprehensive investment in responsive design.&lt;/p&gt;
&lt;p&gt;Resources:
Material Design for Large Screens → https://goo.gle/3dXWdR5&lt;/p&gt;
&lt;p&gt;Speakers: Liam Spradlin, Rody Davis&lt;/p&gt;
&lt;p&gt;Watch more:
Material Design at Google I/O 2021 Playlist → https://goo.gle/io21-MaterialDesign
All Google I/O 2021 Technical Sessions → https://goo.gle/io21-technicalsessions
All Google I/O 2021 Sessions → https://goo.gle/io21-allsessions&lt;/p&gt;
&lt;p&gt;Subscribe to Material Design → https://goo.gle/MaterialDesign-YouTube&lt;/p&gt;
&lt;p&gt;#GoogleIO #Design #Android&lt;/p&gt;
&lt;p&gt;product: Material Design - General; event: Google I/O 2021; fullname: Liam Spradlin, Rody Davis; re_ty: Premiere;&lt;/p&gt;
</content:encoded></item><item><title>Design at scale with Web Components (and ducks)</title><link>https://rodydavis.com/videos/talks/design-at-scale-with-web-components-and-ducks/</link><guid isPermaLink="true">https://rodydavis.com/videos/talks/design-at-scale-with-web-components-and-ducks/</guid><description>Learn how Google uses Web Components to scale design systems across various frameworks, enabling design-once, build-once, use-everywhere capabilities.</description><pubDate>Tue, 22 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Design at scale with Web Components (and ducks)&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/DBcz_bGcHgk&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;You have a beautiful design system that delivers consistent, on-brand user experiences! Now, how do your designers and engineers provide your design to teams using any framework from React, to Vue, to Angular, and more?&lt;/p&gt;
&lt;p&gt;The Material Design team shares how Web Components are a great choice to empower design systems at Google scale.&lt;/p&gt;
&lt;p&gt;Design once. Build once. Use everywhere.&lt;/p&gt;
&lt;p&gt;Resources:
Lion Web Components → https://goo.gle/363MUey
Prefers-color-scheme → https://goo.gle/2UXBp1F
Spectrum Web Components → https://goo.gle/3pZXomU
Quack→ https://goo.gle/quack
Github material-components-web-components → https://goo.gle/mwc&lt;/p&gt;
&lt;p&gt;Speakers: Liz Mitchell, Rody Davis&lt;/p&gt;
&lt;p&gt;Watch all Chrome Developer Summit sessions here → https://goo.gle/cds20-sessions
Subscribe to Google Chrome Developers here → https://goo.gle/ChromeDevs&lt;/p&gt;
&lt;p&gt;#chromedevsummit #chrome #webdesign&lt;/p&gt;
&lt;p&gt;event: Chrome Dev Summit 2020; re_ty: Publish; product: Chrome - General; fullname: Rody Davis;&lt;/p&gt;
</content:encoded></item><item><title>Findevs - Flutter Hackathon 2019 - Orlando Winner</title><link>https://rodydavis.com/videos/talks/findevs-flutter-hackathon-2019-orlando-winner/</link><guid isPermaLink="true">https://rodydavis.com/videos/talks/findevs-flutter-hackathon-2019-orlando-winner/</guid><description>Findevs is a Flutter application developed for the Flutter Hackathon 2019 in Orlando, showcasing innovative mobile development.</description><pubDate>Mon, 03 Jun 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Findevs - Flutter Hackathon 2019 - Orlando Winner&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/cOgYFY6fJiU&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Source: https://github.com/AppleEducate/flutter_devs/tree/hackathon&lt;/p&gt;
</content:encoded></item><item><title>Google Cloud Next 2022, Dart partnership with GitHub, and more dev news!</title><link>https://rodydavis.com/videos/talks/google-cloud-next-2022-dart-partnership-with-github-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/talks/google-cloud-next-2022-dart-partnership-with-github-and-more-dev-news/</guid><description>Google Developer News Show covers Dart&apos;s partnership with GitHub for supply chain security, Google Cloud Next 2022 announcements, and other recent developer updates.</description><pubDate>Thu, 13 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Google Cloud Next 2022, Dart partnership with GitHub, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/Ay5O5H9MAlI&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 315 | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Introduction
0:10 - Partnering with GitHub on supply chain security for Dart packages → https://goo.gle/3EKj2Fs
0:32 - Google Cloud Next 2022 → https://goo.gle/3EKiXSa
0:46 - CircularNet: Reducing waste with Machine Learning → https://goo.gle/3CUjr7g
1:20 - Kick Start Round G → https://goo.gle/3ET648P
1:51 -  Don’t forget to like, comment, and subscribe!&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody from Developer Relations. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow&lt;br&gt;
Subscribe to Google Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
</content:encoded></item><item><title>Google I/O 2023, passkeys passwordless future, and more dev news!</title><link>https://rodydavis.com/videos/talks/google-io-2023-passkeys-passwordless-future-and-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/talks/google-io-2023-passkeys-passwordless-future-and-more-dev-news/</guid><description>Google Developer News Show covers passkeys, Android app optimization for large screens, I/O &apos;23 preparations, and opportunities for developer student clubs.</description><pubDate>Mon, 08 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Google I/O 2023, passkeys passwordless future, and more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/GfUvCl4uCWc&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 340  | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Introduction
0:10 - The beginning of the end of the password → https://goo.gle/41gLgPO
For developers you can learn more → https://goo.gle/3VHW608
For users you can learn more at g.co/passkeys
0:57 - Become a Google Developer Student Club lead → https://goo.gle/3HOVOin
1:27 - How to optimize your Android app for large screens (And what NOT to do!)→ https://goo.gle/3LN6GOS
2:23 -  Get ready for I/O ‘23 → https://goo.gle/3VJWawu
2:46 -  Don’t forget to like, comment, and subscribe!&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody, a Developer Advocate. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow&lt;br&gt;
Subscribe to Google Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
</content:encoded></item><item><title>Material You &amp; Figma: Bringing Dynamic Color &amp; brand together - Rody Davis, Ivy Knight (Schema 2021)</title><link>https://rodydavis.com/videos/talks/material-you-figma-bringing-dynamic-color-brand-together-rody-davis-ivy-knight-schema-2021/</link><guid isPermaLink="true">https://rodydavis.com/videos/talks/material-you-figma-bringing-dynamic-color-brand-together-rody-davis-ivy-knight-schema-2021/</guid><description>Learn how the new Figma plugin integrates Material You&apos;s dynamic color with your design system to create personalized and brand-consistent UI experiences.</description><pubDate>Wed, 10 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Material You &amp;amp; Figma: Bringing Dynamic Color &amp;amp; brand together - Rody Davis, Ivy Knight (Schema 2021)&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/clDuqcKgNBQ&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Material You is enabling a new level of individuality across interfaces. But how does dynamic color interact with the distinct brand expression in your design system? In this session, Rody Davis (Developer Advocate) and Ivy Knight (Designer Advocate) dive into the NEW Figma plugin for generating dynamic color schemes and see those changes reflected throughout your app&apos;s UI.&lt;/p&gt;
&lt;p&gt;Schema is an online conference about design systems by Figma. For more info, check out https://schema.figma.com/&lt;/p&gt;
&lt;p&gt;#figma #schema #schema2021 #design #ProductDesign #FigmaDesign #DesignSystems #MaterialYou #Google #GoogleDesign&lt;/p&gt;
</content:encoded></item><item><title>Moving an Angular app to Flutter web - DevFest FL 2019</title><link>https://rodydavis.com/videos/talks/moving-an-angular-app-to-flutter-web-devfest-fl-2019/</link><guid isPermaLink="true">https://rodydavis.com/videos/talks/moving-an-angular-app-to-flutter-web-devfest-fl-2019/</guid><description>Learn how to migrate an Angular application to Flutter web in a 6-month project, presented at DevFest FL 2019.</description><pubDate>Mon, 18 Nov 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Moving an Angular app to Flutter web - DevFest FL 2019&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/SLT9b7iLisA&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Moving a Angular application to Flutter web in 6 months.&lt;/p&gt;
</content:encoded></item><item><title>Road to Flutter City - Rody Davis</title><link>https://rodydavis.com/videos/talks/road-to-flutter-city-rody-davis/</link><guid isPermaLink="true">https://rodydavis.com/videos/talks/road-to-flutter-city-rody-davis/</guid><description>Rody Davis&apos;s &quot;Road to Flutter City&quot; is a video and accompanying slides explaining how to build a piano app with Flutter, with source code available on GitHub.</description><pubDate>Thu, 20 Jun 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Road to Flutter City - Rody Davis&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/N1PKhvyDYyg&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Source: https://github.com/rodydavis/flutter_piano&lt;/p&gt;
&lt;p&gt;Slides: https://docs.google.com/presentation/d/18FuD56CHamy9jIi66xjvzb-68QA4e3Kr0xIjHRvLKMs/edit?usp=sharing&lt;/p&gt;
</content:encoded></item><item><title>Space Apps Orlando 2019 - Space Curiosity</title><link>https://rodydavis.com/videos/talks/space-apps-orlando-2019-space-curiosity/</link><guid isPermaLink="true">https://rodydavis.com/videos/talks/space-apps-orlando-2019-space-curiosity/</guid><description>Space Apps Orlando 2019: Learn how to build a cross-platform application with Flutter for space exploration challenges.</description><pubDate>Sat, 19 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Space Apps Orlando 2019 - Space Curiosity&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/InYfMPVQ4Wo&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;Building a global, cross platform application with Flutter&lt;/p&gt;
</content:encoded></item><item><title>Women in Machine Learning Symposium, Flutter Forward, more dev news!</title><link>https://rodydavis.com/videos/talks/women-in-machine-learning-symposium-flutter-forward-more-dev-news/</link><guid isPermaLink="true">https://rodydavis.com/videos/talks/women-in-machine-learning-symposium-flutter-forward-more-dev-news/</guid><description>Google Developer News Show covering women in machine learning, Flutter Forward, and the latest web development updates.</description><pubDate>Mon, 05 Dec 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Women in Machine Learning Symposium, Flutter Forward, more dev news!&lt;/h1&gt;
&lt;p&gt;&amp;lt;div class=&amp;quot;video-container&amp;quot;&amp;gt;
&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/E8EMabhmN_E&amp;quot; title=&amp;quot;YouTube video player&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;p&gt;TL;DR 321 | The Google Developer News Show&lt;/p&gt;
&lt;p&gt;0:00 - Introduction
0:10 -  Web: The large, small, and dynamic viewport units → https://goo.gle/3FrLCeK
0:32 - Join us at the 2nd Women in Machine Learning Symposium → https://goo.gle/3gVBIJb
0:51 - Announcing Flutter Forward → https://goo.gle/3F4xnet
1:08 - Don’t forget to like, comment, and subscribe!&lt;/p&gt;
&lt;p&gt;Here to bring you the latest developer news from across Google is Rody from Developer Relations. Tune in every week for a new episode, and let us know what you think of the latest announcements in the comments below.&lt;/p&gt;
&lt;p&gt;Follow Google Developers on Instagram → https://goo.gle/googledevs&lt;/p&gt;
&lt;p&gt;Watch more #DevShow → https://goo.gle/GDevShow&lt;br&gt;
Subscribe to Google Developers → https://goo.gle/developers&lt;/p&gt;
&lt;p&gt;#Google #Developers&lt;/p&gt;
</content:encoded></item><item><title>Color Utilities in JavaScript</title><link>https://rodydavis.com/web/snippets/color-utilities/</link><guid isPermaLink="true">https://rodydavis.com/web/snippets/color-utilities/</guid><description>JavaScript color utility functions for converting between RGB, HEX, and HSL color formats.</description><pubDate>Sun, 19 Jan 2025 03:37:33 GMT</pubDate><content:encoded>&lt;h1&gt;Color Utilities in JavaScript&lt;/h1&gt;
&lt;p&gt;Color utilities generated by &lt;a href=&quot;https://github.com/features/copilot&quot;&gt;GitHub Copilot&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Convert an RGB color to HSL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function rgbToHsl(r, g, b) {
    r /= 255;
    g /= 255;
    b /= 255;
    var max = Math.max(r, g, b);
    var min = Math.min(r, g, b);
    var h, s, l = (max + min) / 2;
    if (max == min) {
        h = s = 0;
    } else {
        var d = max - min;
        s = l &amp;gt; 0.5 ? d / (2 - max - min) : d / (max + min);
        switch (max) {
            case r:
                h = (g - b) / d + (g &amp;lt; b ? 6 : 0);
                break;
            case g:
                h = (b - r) / d + 2;
                break;
            case b:
                h = (r - g) / d + 4;
                break;
        }
        h /= 6;
    }
    return [h, s, l];
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Convert HEX to RGB&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function hexToRgb(hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Convert HSL to HEX&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function hslToHex(h, s, l) {
    var r, g, b;
    if (s == 0) {
        r = g = b = l;
    } else {
        var hue2rgb = function hue2rgb(p, q, t) {
            if (t &amp;lt; 0) t += 1;
            if (t &amp;gt; 1) t -= 1;
            if (t &amp;lt; 1 / 6) return p + (q - p) * 6 * t;
            if (t &amp;lt; 1 / 2) return q;
            if (t &amp;lt; 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
            return p;
        };
        var q = l &amp;lt; 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
        r = hue2rgb(p, q, h + 1 / 3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1 / 3);
    }
    return &amp;quot;#&amp;quot; + (1 &amp;lt;&amp;lt; 24 | r &amp;lt;&amp;lt; 16 | g &amp;lt;&amp;lt; 8 | b).toString(16).slice(1);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Material 3 to Material 2 Theme Adapter</title><link>https://rodydavis.com/web/snippets/m3-to-m2-adapter/</link><guid isPermaLink="true">https://rodydavis.com/web/snippets/m3-to-m2-adapter/</guid><description>Apply Material 3 theming to Material 2 components using CSS variables for consistent styling with current Material Design guidelines.</description><pubDate>Sun, 19 Jan 2025 05:46:33 GMT</pubDate><content:encoded>&lt;h1&gt;Material 3 to Material 2 Theme Adapter&lt;/h1&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;How to style Material 2 components with Material 3 in CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root {
  --mdc-theme-primary: var(--md-sys-color-primary);
  --mdc-theme-on-primary: var(--md-sys--coloron-primary);
  --mdc-theme-background: var(--md-sys--colorbackground);
  --mdc-theme-on-background: var(--md-sys--coloron-background);
  --mdc-theme-on-surface-variant: var(--md-sys--coloron-surface-variant);
  --mdc-theme-surface-variant: var(--md-sys--colorsurface-variant);
  --mdc-theme-on-surface: var(--md-sys--coloron-surface);
  --mdc-theme-surface: var(--md-sys--colorsurface);
  --mdc-theme-text-primary-on-background: var(--md-sys--coloron-surface-variant);
  --mdc-theme-outline: var(--md-sys-color-outline);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>