<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://anhtuanmai.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://anhtuanmai.github.io/" rel="alternate" type="text/html" /><updated>2026-05-07T09:14:04+02:00</updated><id>https://anhtuanmai.github.io/feed.xml</id><title type="html">Anh Tuan</title><subtitle>Personal site of Anh Tuan Mai — software engineer, based in Paris.</subtitle><author><name>Anh Tuan Mai</name></author><entry><title type="html">My blog, powered by Jekyll and GitHub Pages</title><link href="https://anhtuanmai.github.io/meta/jekyll-blog-ideas/" rel="alternate" type="text/html" title="My blog, powered by Jekyll and GitHub Pages" /><published>2026-04-27T00:00:00+02:00</published><updated>2026-04-27T00:00:00+02:00</updated><id>https://anhtuanmai.github.io/meta/jekyll-blog-ideas</id><content type="html" xml:base="https://anhtuanmai.github.io/meta/jekyll-blog-ideas/"><![CDATA[<p>I keep notes for myself — mostly technical problems I’ve hit, topics I’m digging into, and the occasional event worth remembering. Lately I’ve been writing more, and it has shifted from notes-for-me into something I might actually share. I’m in a book club that runs on Notion, but when I compared the options for my own space, I went with GitHub Pages and Jekyll. I’m more of a builder than a blogger, and the toolchain matters to me.</p>

<h2 id="why-not-the-others">Why not the others?</h2>

<ul>
  <li><strong>Notion</strong> — great for collaboration, weak as a public publishing platform.</li>
  <li><strong>Medium</strong> — clean reader experience, but the content lives on someone else’s terms.</li>
  <li><strong>Substack</strong> — newsletter-first; I don’t want to optimise for an inbox.</li>
  <li><strong>WordPress</strong> — too much surface area for what I need.</li>
</ul>

<p>GitHub Pages + Jekyll wins for me on one axis the others can’t match: <strong>full ownership of the site structure, source files, and publishing workflow</strong>. The repo <em>is</em> the blog. Version control, diffs, PRs, and deploys are the same workflow I already use every day.</p>

<h2 id="what-is-jekyll">What is Jekyll?</h2>

<blockquote>
  <p>Jekyll is a simple, blog-aware, static site generator perfect for personal, project, or organization sites. Think of it like a file-based CMS, without all the complexity.
— <a href="https://github.com/jekyll/jekyll">jekyll/jekyll</a></p>
</blockquote>

<p>It’s Ruby-based and tightly integrated with GitHub Pages — push to <code class="language-plaintext highlighter-rouge">main</code>, GitHub builds and serves the site. No CI to wire up, no server to maintain.</p>

<h2 id="whats-next">What’s next?</h2>

<p>I’ll keep writing here, directly in this repo. I also want to migrate some old notes over, which will take time. And I’m tempted to try a GitHub Pages + Astro setup at some point, just to see how it compares. We’ll see.</p>

<p>Stay tuned.</p>]]></content><author><name>Anh Tuan Mai</name></author><category term="meta" /><category term="github" /><category term="jekyll" /><category term="blog" /><summary type="html"><![CDATA[Why I picked GitHub Pages + Jekyll over Notion, Medium, Substack and WordPress for my personal blog.]]></summary></entry><entry><title type="html">From MVP to MVI on Android — borrowing from Redux</title><link href="https://anhtuanmai.github.io/engineering/from-mvp-to-mvi-on-android/" rel="alternate" type="text/html" title="From MVP to MVI on Android — borrowing from Redux" /><published>2019-12-15T00:00:00+01:00</published><updated>2019-12-15T00:00:00+01:00</updated><id>https://anhtuanmai.github.io/engineering/from-mvp-to-mvi-on-android</id><content type="html" xml:base="https://anhtuanmai.github.io/engineering/from-mvp-to-mvi-on-android/"><![CDATA[<p>For most of the year, the team has been quietly migrating our Android app from <strong>MVP</strong>
to <strong>MVI</strong>. The new pieces are inspired by <strong>Redux</strong> on the web side, adapted to fit
Kotlin and the Android lifecycle. This is a short note on why we made the move and how
it looks in practice.</p>

<h2 id="what-mvp-cost-us">What MVP cost us</h2>

<p>Our MVP setup looked clean on paper: a Presenter per screen, a View interface, a Model
behind a use case. In practice, three things kept biting us.</p>

<ul>
  <li><strong>State was scattered.</strong> A screen’s state lived across the Presenter’s fields, the
View’s widgets, and a few flags in the Model. Reproducing a bug meant reproducing a
combination, not a value.</li>
  <li><strong>The View talked back too much.</strong> <code class="language-plaintext highlighter-rouge">view.showLoading()</code>, <code class="language-plaintext highlighter-rouge">view.showError()</code>,
<code class="language-plaintext highlighter-rouge">view.hideError()</code>, <code class="language-plaintext highlighter-rouge">view.showContent()</code> — the Presenter ended up driving the UI step
by step instead of describing it.</li>
  <li><strong>Lifecycle leaks were a constant.</strong> Presenters outlived the View on rotation; the
View came back without its previous state; we patched it screen by screen.</li>
</ul>

<p>By the third or fourth time we wrote the same “restore state after rotation” code, we
agreed it was time for something else.</p>

<h2 id="what-mvi-gave-us">What MVI gave us</h2>

<p>MVI flips the relationship. Instead of the Presenter calling methods on the View, the
View <strong>renders a single state object</strong>, and that state can only be changed by emitting
an <strong>intent</strong>. Three rules:</p>

<ol>
  <li><strong>One state per screen.</strong> Everything the screen needs to render lives in one
immutable <code class="language-plaintext highlighter-rouge">State</code> data class.</li>
  <li><strong>State changes only through reducers.</strong> An intent (user action, result, error)
goes through a pure <code class="language-plaintext highlighter-rouge">(State, Intent) -&gt; State</code> function — same idea as a Redux
reducer.</li>
  <li><strong>Side effects are isolated.</strong> Network calls, database writes, navigation events
live outside the reducer and are turned back into intents when they finish.</li>
</ol>

<p>The result is a screen that’s basically a function: <code class="language-plaintext highlighter-rouge">state -&gt; UI</code>. Bugs become
reproducible because the state is one value, not a constellation.</p>

<h2 id="how-it-looks-in-kotlin">How it looks in Kotlin</h2>

<p>Roughly:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">data class</span> <span class="nc">SearchState</span><span class="p">(</span>
    <span class="kd">val</span> <span class="py">query</span><span class="p">:</span> <span class="nc">String</span> <span class="p">=</span> <span class="s">""</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">items</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Item</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">emptyList</span><span class="p">(),</span>
    <span class="kd">val</span> <span class="py">loading</span><span class="p">:</span> <span class="nc">Boolean</span> <span class="p">=</span> <span class="k">false</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">error</span><span class="p">:</span> <span class="nc">Throwable</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
<span class="p">)</span>

<span class="k">sealed</span> <span class="kd">class</span> <span class="nc">SearchIntent</span> <span class="p">{</span>
    <span class="kd">data class</span> <span class="nc">QueryChanged</span><span class="p">(</span><span class="kd">val</span> <span class="py">q</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">:</span> <span class="nc">SearchIntent</span><span class="p">()</span>
    <span class="kd">object</span> <span class="nc">Submit</span> <span class="p">:</span> <span class="nc">SearchIntent</span><span class="p">()</span>
    <span class="kd">data class</span> <span class="nc">ResultsLoaded</span><span class="p">(</span><span class="kd">val</span> <span class="py">items</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Item</span><span class="p">&gt;)</span> <span class="p">:</span> <span class="nc">SearchIntent</span><span class="p">()</span>
    <span class="kd">data class</span> <span class="nc">Failed</span><span class="p">(</span><span class="kd">val</span> <span class="py">cause</span><span class="p">:</span> <span class="nc">Throwable</span><span class="p">)</span> <span class="p">:</span> <span class="nc">SearchIntent</span><span class="p">()</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nf">reduce</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="nc">SearchState</span><span class="p">,</span> <span class="n">intent</span><span class="p">:</span> <span class="nc">SearchIntent</span><span class="p">):</span> <span class="nc">SearchState</span> <span class="p">=</span> <span class="k">when</span> <span class="p">(</span><span class="n">intent</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">is</span> <span class="nc">QueryChanged</span>   <span class="p">-&gt;</span> <span class="n">state</span><span class="p">.</span><span class="nf">copy</span><span class="p">(</span><span class="n">query</span> <span class="p">=</span> <span class="n">intent</span><span class="p">.</span><span class="n">q</span><span class="p">)</span>
    <span class="k">is</span> <span class="nc">Submit</span>         <span class="p">-&gt;</span> <span class="n">state</span><span class="p">.</span><span class="nf">copy</span><span class="p">(</span><span class="n">loading</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span> <span class="n">error</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span>
    <span class="k">is</span> <span class="nc">ResultsLoaded</span>  <span class="p">-&gt;</span> <span class="n">state</span><span class="p">.</span><span class="nf">copy</span><span class="p">(</span><span class="n">loading</span> <span class="p">=</span> <span class="k">false</span><span class="p">,</span> <span class="n">items</span> <span class="p">=</span> <span class="n">intent</span><span class="p">.</span><span class="n">items</span><span class="p">)</span>
    <span class="k">is</span> <span class="nc">Failed</span>         <span class="p">-&gt;</span> <span class="n">state</span><span class="p">.</span><span class="nf">copy</span><span class="p">(</span><span class="n">loading</span> <span class="p">=</span> <span class="k">false</span><span class="p">,</span> <span class="n">error</span> <span class="p">=</span> <span class="n">intent</span><span class="p">.</span><span class="n">cause</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The screen subscribes to a stream of <code class="language-plaintext highlighter-rouge">SearchState</code> (we use <strong>RxJava</strong> today; we’re
watching Kotlin <strong>Flow</strong>, which stabilized this fall) and re-renders on every emission.
Side effects — the actual network call triggered by <code class="language-plaintext highlighter-rouge">Submit</code> — live in a separate
“effect handler” that maps an intent to an observable of follow-up intents.</p>

<p>That’s the whole pattern. There’s no per-method dance with the View anymore.</p>]]></content><author><name>Anh Tuan Mai</name></author><category term="engineering" /><category term="android" /><category term="kotlin" /><category term="mvi" /><category term="mvp" /><category term="redux" /><category term="architecture" /><summary type="html"><![CDATA[How we replaced our MVP screens with an MVI architecture inspired by Redux — one state, one stream, fewer surprises.]]></summary></entry><entry><title type="html">Vœux pour Exyzt — A thank-you to my first team</title><link href="https://anhtuanmai.github.io/personal/voeux-pour-exyzt/" rel="alternate" type="text/html" title="Vœux pour Exyzt — A thank-you to my first team" /><published>2018-12-25T00:00:00+01:00</published><updated>2018-12-25T00:00:00+01:00</updated><id>https://anhtuanmai.github.io/personal/voeux-pour-exyzt</id><content type="html" xml:base="https://anhtuanmai.github.io/personal/voeux-pour-exyzt/"><![CDATA[<p>At the end of 2018, I left Castres and moved to Paris to start a new chapter of my career.
Before turning the page, I wanted to send a few words to the people who shaped my very first
working years — my colleagues and bosses at <strong>Exyzt</strong>. This post is a small archive of that
message, kept here so I never forget where I started.</p>

<h2 id="a-thank-you-to-my-first-boss-and-team">A thank you to my first boss and team</h2>

<p>I want to mark a special thank you to <strong>M. Christian Gendreault</strong>, my very first boss.
He gave me a chance, trusted a young engineer fresh out of school, and showed me what a
good director and a good adviser looks like. Three years under his wing taught me more
than I realized at the time.</p>

<p>I also want to thank my project lead <strong>Jean-Christophe</strong>, whose work and guidance I
particularly admired, and every colleague who worked alongside me and put up with me
(in alphabetical order, <em>lul</em>): Bénédicte, Charlotte, Charline, Dimitri, Flora, Jules,
Mikaël, Paul, Sébastien, and Vincent.</p>

<p>You all made those three years in Castres genuinely good ones.</p>

<h2 id="the-original-message-december-2018-in-french">The original message (December 2018, in French)</h2>

<blockquote>
  <p>J’ai passé trois années avec vous, trois années bien agréables. Vous avez été très
gentils avec moi et vous m’avez beaucoup aidé.</p>

  <p>Merci beaucoup à M. Christian Gendreault qui a été pour moi un bon directeur, un bon
conseiller.</p>

  <p>J’ai particulièrement apprécié le travail de mon chef de projet Jean-Christophe et je
le remercie aussi.</p>

  <p>Je remercie aussi les collaborateurs qui ont travaillé avec moi et qui m’ont supporté :
Bénédicte, Charlotte, Charline, Flora, Dimitri, Mikael, Jules, Paul, Vincent, Sébastien
(l’ordre alphabétique, <em>lul</em>) !!!</p>

  <p>Maintenant, je me plais bien à Paris, même si c’est différent de Castres.</p>

  <p>Je vous souhaite à tous une très bonne année 2019.</p>
</blockquote>

<h2 id="looking-back">Looking back</h2>

<p>Paris was a big change from Castres — bigger pace, bigger noise, bigger distances. But
the foundations I built at Exyzt traveled with me. To everyone there: thank you, and a
belated <em>bonne année</em> — every year since.</p>]]></content><author><name>Anh Tuan Mai</name></author><category term="personal" /><category term="exyzt" /><category term="gratitude" /><category term="first-job" /><category term="castres" /><summary type="html"><![CDATA[On Christmas 2018, just after moving from Castres to Paris, I sent a New Year's message to my colleagues at Exyzt — my very first company. This post keeps that note alive.]]></summary></entry><entry><title type="html">Clean Architecture: a direction, not a rewrite</title><link href="https://anhtuanmai.github.io/engineering/reading-clean-architecture/" rel="alternate" type="text/html" title="Clean Architecture: a direction, not a rewrite" /><published>2018-05-22T00:00:00+02:00</published><updated>2018-05-22T00:00:00+02:00</updated><id>https://anhtuanmai.github.io/engineering/reading-clean-architecture</id><content type="html" xml:base="https://anhtuanmai.github.io/engineering/reading-clean-architecture/"><![CDATA[<p>If you only read one architecture article this year, make it <a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">Uncle Bob’s <em>The Clean
Architecture</em></a>.
I just finished it and I haven’t been this excited about a blog post in a long time —
excited enough to write this one, because I think every team should at least know what
it asks of us.</p>

<h2 id="the-idea-in-one-sentence">The idea, in one sentence</h2>

<p><strong>Business rules in the center, frameworks on the outside, dependencies always
pointing inward.</strong> The database, the UI, the network, the web framework — all of them
are <em>details</em>, plugged in from the edges. The core of the app shouldn’t know they
exist.</p>

<p>That one rule does a surprising amount of work:</p>

<ul>
  <li>You can <strong>swap the database</strong> without touching the rules.</li>
  <li>You can <strong>test the rules</strong> without booting the framework, the device, or the network.</li>
  <li>You can <strong>change the UI</strong> — or run two of them — without rewriting the logic behind it.</li>
  <li>The shape of the code starts to look like the shape of the <strong>business</strong>, not the
shape of your tools.</li>
</ul>

<p>Read like that, half of the bugs we keep fighting stop looking like bugs and start
looking like the consequence of letting frameworks reach into the core. We’re paying a
tax we never agreed to.</p>

<h2 id="so-why-isnt-this-everywhere-already">So why isn’t this everywhere already?</h2>

<p>Because it sounds, at first, like a rewrite. And rewrites are scary, expensive, and
usually a bad idea on a project that’s already shipping.</p>

<p>Here’s the part I want to promote: <strong>Clean Architecture is not a rewrite. It’s a
direction.</strong> You don’t need a green field. You don’t need a six-month freeze. You need
to know which way is “in” and start pointing your new arrows that way.</p>

<h2 id="why-im-writing-this-down">Why I’m writing this down</h2>

<p>I’m writing this because I want the team to read the original article. Not my summary
— the source. Half an hour of reading, and the way you look at our code afterward is
different.</p>

<p>A big rewrite is not always the answer. <strong>A direction is.</strong> Uncle Bob gave us one.
Let’s start walking.</p>]]></content><author><name>Anh Tuan Mai</name></author><category term="engineering" /><category term="architecture" /><category term="clean-architecture" /><category term="refactoring" /><category term="reading" /><summary type="html"><![CDATA[Uncle Bob's Clean Architecture isn't a weekend rewrite — it's a direction you can start walking today, even on a project that's already in flight.]]></summary></entry><entry><title type="html">Building a custom Android keyboard at Exyzt</title><link href="https://anhtuanmai.github.io/engineering/custom-android-keyboard-for-exyzt/" rel="alternate" type="text/html" title="Building a custom Android keyboard at Exyzt" /><published>2016-09-18T00:00:00+02:00</published><updated>2016-09-18T00:00:00+02:00</updated><id>https://anhtuanmai.github.io/engineering/custom-android-keyboard-for-exyzt</id><content type="html" xml:base="https://anhtuanmai.github.io/engineering/custom-android-keyboard-for-exyzt/"><![CDATA[<p>At <strong>Exyzt</strong>, I’ve been working on something a bit unusual: our own
<strong>Android keyboard</strong>, shipped with the company’s pro app.</p>

<h2 id="why-we-needed-one">Why we needed one</h2>

<p>Our field devices run <strong>plain AOSP</strong> — no Google services, no Play Store, just the
stock open-source build. That means the only keyboard out of the box is the basic
<strong>AOSP LatinIME</strong>. It’s fine for casual texting; it’s not fine for our work.</p>

<h2 id="what-we-built">What we built</h2>

<p>A small keyboard app that fits the work. Agents type so much faster now.</p>]]></content><author><name>Anh Tuan Mai</name></author><category term="engineering" /><category term="android" /><category term="keyboard" /><category term="exyzt" /><category term="mobile" /><category term="aosp" /><summary type="html"><![CDATA[Customized keyboard on Android AOSP.]]></summary></entry></feed>