<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Orestis Stylianidis</title>
    <description>The latest articles on DEV Community by Orestis Stylianidis (@ostyl51).</description>
    <link>https://dev.to/ostyl51</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4182248%2F3ff11af9-c4e1-45e4-8c40-a5c5ca63caea.jpg</url>
      <title>DEV Community: Orestis Stylianidis</title>
      <link>https://dev.to/ostyl51</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ostyl51"/>
    <language>en</language>
    <item>
      <title>My app passed 1,100 tests. Six minutes on a real phone found five bugs.</title>
      <dc:creator>Orestis Stylianidis</dc:creator>
      <pubDate>Wed, 04 Nov 2026 19:11:49 +0000</pubDate>
      <link>https://dev.to/ostyl51/my-app-passed-1100-tests-six-minutes-on-a-real-phone-found-five-bugs-5ggo</link>
      <guid>https://dev.to/ostyl51/my-app-passed-1100-tests-six-minutes-on-a-real-phone-found-five-bugs-5ggo</guid>
      <description>&lt;p&gt;I'm building a budgeting app called Budget Lover. It is a personal project, written in Flutter with Firebase behind it, and it tries to answer one question: &lt;em&gt;how much can I spend today?&lt;/em&gt; At its heart is a single "Safe to Spend" number, worked out from the balance, the bills that are coming and the person's typical spending. It speaks 15 languages, and it isn't released yet: so far it lives on an emulator and on my own phone.&lt;/p&gt;

&lt;p&gt;A few days ago I installed the latest build on that phone, ran it in Greek, and spent six minutes tapping through it while taking screenshots and recording the screen. The screenshots and the recording showed five bugs. The test suite, more than 1,100 tests, had passed earlier that day.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Two of the app's six tabs: Insights, where the "Safe to Spend" number lives, and Savings.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This isn't an argument against automated tests. Every bug below now has one. It's a record of what my tests were blind to, because I suspect many apps share the same blind spots.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tests covered
&lt;/h2&gt;

&lt;p&gt;The suite includes layout tests that open every screen and bottom sheet in all 15 languages, at a normal and an enlarged text size and on a small 320 dp screen, and fail on any overflow. That net had already caught plenty of long German and Greek labels. It did not catch these.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A word that didn't fit
&lt;/h2&gt;

&lt;p&gt;The Home screen has four quick actions with a short caption under each. In English the captions are short. In Greek, "Add savings" is &lt;em&gt;Προσθήκη αποταμίευσης&lt;/em&gt;, and its last word is wider than a quarter of the screen, so the app showed "αποταμίευσ…".&lt;/p&gt;

&lt;p&gt;My layout tests fail on overflow, but a caption cut short with an ellipsis is not an overflow: Flutter did exactly what I had asked it to do. The caption now shrinks until its longest word fits, and a new test checks whether any caption was truncated, in every language.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;The same four quick actions in four languages. The long Greek and German captions now fit.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. English inside a Greek screen
&lt;/h2&gt;

&lt;p&gt;The same screenshot showed a sentence in English on the trip card. In the app, the English text is the key to its translation, and a script scans the code for those keys and complains about any that a language table is missing. This sentence was picked by a condition (&lt;code&gt;isEmpty ? 'a' : 'b'&lt;/code&gt;), which the script can't see.&lt;/p&gt;

&lt;p&gt;There are now two defences: a second scanner for exactly that pattern, which found eight texts hiding the same way, and a test that opens every screen in Greek and fails if any text is missing from the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A dialog nobody opened
&lt;/h2&gt;

&lt;p&gt;On Sundays the app offers a short weekly review in a dialog with an icon and a title. In Greek the title, &lt;em&gt;Κυριακάτικη ανασκόπηση&lt;/em&gt;, ran out of the dialog on the right. No test had ever opened that dialog, because it only appears on Sundays. It is now part of the layout tests like every other sheet, and in Greek it overflowed by 96 pixels the first time it was asked.&lt;/p&gt;

&lt;p&gt;The lesson: whatever appears conditionally (on Sundays, for one kind of account, on first launch) needs a deliberate way to be reached from a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The screen that kept reloading
&lt;/h2&gt;

&lt;p&gt;This one was my own doing, and only the recording showed it. Every time the keyboard opened on the sign-in screen, the loading screen flashed over the form and the text I had typed disappeared.&lt;/p&gt;

&lt;p&gt;I had just added a soft cross-fade between the loading, sign-in and app screens. The gate behind them asked Firebase "who is signed in?" inside its build method, which creates a new stream on every rebuild, and a new stream starts out as "waiting", which the app shows as the loading screen. The keyboard animation triggered a rebuild on every frame, and the new cross-fade made each of those moments visible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: a new stream every time the screen rebuilds&lt;/span&gt;
&lt;span class="n"&gt;StreamBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nl"&gt;stream:&lt;/span&gt; &lt;span class="n"&gt;FirebaseAuth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authStateChanges&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;

&lt;span class="c1"&gt;// After: asked once, kept for the life of the screen&lt;/span&gt;
&lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;_auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FirebaseAuth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authStateChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;StreamBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nl"&gt;stream:&lt;/span&gt; &lt;span class="n"&gt;_auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more useful change was the test. It types into the form, moves the keyboard frame by frame, rebuilds the parent, and checks that the loading screen never appears and the text stays. Then I put the old code back for a minute to make sure the test fails against it. It did.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;The sign-in screen in English and Greek, in the app's brand colours.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A login that only failed in the real build
&lt;/h2&gt;

&lt;p&gt;Not a language problem, but the same lesson. The Google sign-in button failed on my phone with an error about a missing server client id. The Google plugin looks that value up in a string resource by name, and the release build's resource shrinker removes strings that look unused. My debug builds carry the string; the release build did not. The app now passes the id explicitly, and a test checks it against the Firebase configuration file.&lt;/p&gt;

&lt;h2&gt;
  
  
  And a sixth, while writing this
&lt;/h2&gt;

&lt;p&gt;While taking the screenshots for this article I found one more. Arabic reads right to left, so the first tab sits on the right of the tab strip. Its highlight stayed on the left, under the wrong tab. It was a one-word fix (a direction-aware alignment instead of a fixed one), and a test now checks the highlight under every tab in both directions.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;One screen in six of the 15 languages, including Arabic, which reads right to left.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in how I work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests mirror what a user sees.&lt;/strong&gt; Every screen, every language, three screen and text-size combinations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every bug leaves a test behind,&lt;/strong&gt; and where I can, I check that the test fails on the old code before I trust it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real-device pass stays in the loop,&lt;/strong&gt; and it is a recording, not just screenshots. The flicker was invisible in a still image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translated text is product code.&lt;/strong&gt; The English string is the key, and a script refuses any language table with a missing key or mismatched placeholders.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Questions I'm still thinking about
&lt;/h2&gt;

&lt;p&gt;Translation quality is the part I can judge least. I read Greek and English; for the other thirteen languages the checks tell me the text is complete and that it fits, not that it sounds natural. If you speak one of them, I'd be curious whether the wording feels like a native app or like a translation.&lt;/p&gt;

&lt;p&gt;The number on that orange card is the heart of the app, and I keep wondering what makes a person trust a figure like that. If you've ever tried to budget, what would you want to see behind it?&lt;/p&gt;

&lt;p&gt;And on the engineering side: when did a real device last prove your test suite wrong, and what did you add afterwards?&lt;/p&gt;

&lt;p&gt;The app is still in testing, so none of these decisions is final.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I drafted this article with the help of an AI assistant and edited it myself.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>testing</category>
      <category>mobile</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a budgeting app where the AI never does the math</title>
      <dc:creator>Orestis Stylianidis</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:06:34 +0000</pubDate>
      <link>https://dev.to/ostyl51/i-built-a-budgeting-app-where-the-ai-never-does-the-math-3j6e</link>
      <guid>https://dev.to/ostyl51/i-built-a-budgeting-app-where-the-ai-never-does-the-math-3j6e</guid>
      <description>&lt;p&gt;Most budgeting apps answer one question: &lt;em&gt;what did I spend?&lt;/em&gt; The question I actually ask myself is different: &lt;em&gt;how much can I spend today without regretting it on the 28th?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Budget Lover&lt;/strong&gt;, an Android app (Flutter + Firebase + Gemini) that answers that second question with one number, a daily "safe to spend" amount, and an AI advisor you can ask follow-up questions. I'm a Computer Science student and this is a solo project. It is not released yet, so everything below is from building and testing it on an emulator with demo data.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The one number: safe to spend
&lt;/h2&gt;

&lt;p&gt;The core idea is simple. Take what is in the account and subtract everything that is already spoken for until the next payday:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// simplified from my engine&lt;/span&gt;
&lt;span class="n"&gt;safe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt;
     &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;upcomingBills&lt;/span&gt;
     &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;
     &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;plannedPayments&lt;/span&gt;
     &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;savingsContribution&lt;/span&gt;
     &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;safetyBuffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;dailyAllowance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;daysUntilPayday&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is left is money you can spend freely, and dividing it by the days until the next income gives a daily allowance. The real version also handles things like a monthly budget cap, trips, debt payments and subscriptions detected from your history, but the shape is the same.&lt;/p&gt;

&lt;p&gt;I put all of this in a pure Dart "engine" layer with no Flutter or Firebase imports. That turned out to be the best decision of the project, because it can be unit tested in milliseconds, and the app has a lot of edge cases (month ends, paydays that move, a bill that lands tomorrow).&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI explains, the code calculates
&lt;/h2&gt;

&lt;p&gt;I wanted an advisor that can answer "Can I afford a €80 purchase?", but language models are unreliable at arithmetic and happy to invent numbers that sound right. So the AI never does the math.&lt;/p&gt;

&lt;p&gt;When the user asks a question, the app first works out the relevant &lt;strong&gt;facts&lt;/strong&gt; locally: the current safe-to-spend, what it would become after the purchase, the new daily allowance, upcoming bills. Only then does it send those facts to Gemini with the question, and ask it to explain the result in plain language.&lt;/p&gt;

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

&lt;p&gt;The advisor's answer in the screenshot above ("new safe to spend €290, verdict: tight") is built on numbers my engine produced. The model's job is tone and explanation, not accounting. A side effect: the facts are just totals and category names, so the model never needs to see individual transactions.&lt;/p&gt;

&lt;p&gt;The same engine powers a &lt;strong&gt;"what if?" simulator&lt;/strong&gt;. It runs the calculation twice, once with your real data and once with a change (spend €150, cancel a subscription, earn less), and shows the difference. Nothing is saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  No API key in the app
&lt;/h2&gt;

&lt;p&gt;A mistake I made early on was shipping an AI key inside the app. Anything inside an APK can be extracted. I moved to &lt;strong&gt;Firebase AI Logic&lt;/strong&gt;, which sits between the app and Gemini, so the app contains no key at all. Combined with App Check, requests only work from the real app.&lt;/p&gt;

&lt;p&gt;It has a cost too: free-tier limits are low, so the advisor has a daily message counter (you can see "10 left" in the screenshot) and I use a lighter, cheaper model for small jobs like reading receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller features that taught me the most
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Receipt scanning:&lt;/strong&gt; a photo goes to a lightweight Gemini model, which returns the amount, merchant and category. The parsing of its answer is regular code with validation, so a bad answer cannot corrupt data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscriptions:&lt;/strong&gt; regular payments are collected in one place with their yearly cost. The app works out the kind (streaming, music, cloud...) from the name, on the phone, so it can spot overlaps like three streaming services and tell the AI the kinds without the names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trips:&lt;/strong&gt; a budget per trip, per-day and per-person costs, and live currency conversion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly wrap-up:&lt;/strong&gt; story-style cards you can export to PDF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XP and badges:&lt;/strong&gt; small rewards for good habits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15 languages&lt;/strong&gt;, including right-to-left Arabic. I wrote my own small translation layer and layout tests that fail when a translation is too long for its box.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd tell my past self
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep the logic out of the widgets.&lt;/strong&gt; The engine layer paid for itself within a week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't let the model do what a function can do.&lt;/strong&gt; Use it for language, not for numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the boring parts.&lt;/strong&gt; The project has more than 50 test files, and most bugs I found were date edge cases, not UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo data matters.&lt;/strong&gt; I built a demo account so I could take screenshots and test without touching real data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;Flutter and Dart, Provider for state, Firebase Authentication and Firestore, Firebase AI Logic with Gemini, and Flutter's PDF and printing packages.&lt;/p&gt;

&lt;p&gt;I'm looking for freelance work building apps and websites like this one. If you'd like to see more of my work, or you have an app idea, you can find me through my portfolio.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Portfolio: &lt;a href="https://ostyl.dev" rel="noopener noreferrer"&gt;https://ostyl.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>firebase</category>
      <category>ai</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
