<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Igor&#x27;s corner</title>
    <subtitle>A public repository of things I know I will want to reference later, and you 🧑‍🔧 may find useful too.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://shtein.me/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://shtein.me/"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-15T15:02:00+03:00</updated>
    <id>https://shtein.me/atom.xml</id>
    <entry xml:lang="en">
        <title>REST vs gRPC for a public API in 2026</title>
        <published>2026-06-15T15:02:00+03:00</published>
        <updated>2026-06-15T15:02:00+03:00</updated>
        
        <author>
          <name>
            
              Igor Shtein
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shtein.me/posts/rest-vs-grpc-for-public-api/"/>
        <id>https://shtein.me/posts/rest-vs-grpc-for-public-api/</id>
        
        <content type="html" xml:base="https://shtein.me/posts/rest-vs-grpc-for-public-api/">&lt;h1 id=&quot;the-problem&quot;&gt;The problem&lt;&#x2F;h1&gt;
&lt;p&gt;I am adding a server to my &lt;a href=&quot;&#x2F;tags&#x2F;flutter&quot;&gt;Flutter&lt;&#x2F;a&gt; app &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;doshboard.com&#x2F;&quot;&gt;Doshboard&lt;&#x2F;a&gt; (intentionally overengineered for learning - don&#x27;t do this in your pet projects). Internally the microservices already talk &lt;a href=&quot;&#x2F;tags&#x2F;grpc&quot;&gt;gRPC&lt;&#x2F;a&gt;, but the public API conventionally uses REST, so I put a REST - gRPC transcoder in front of them.&lt;&#x2F;p&gt;
&lt;p&gt;Do I need that transcoder though? My users wouldn&#x27;t mind lower data usage and better streaming, and I wouldn&#x27;t mind deleting a translation layer - especially that &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;grpc.io&#x2F;docs&#x2F;languages&#x2F;dart&#x2F;&quot;&gt;Dart is officially supported&lt;&#x2F;a&gt;. So the question for this post: &lt;strong&gt;can I drop the transcoder and run gRPC end-to-end in 2026, or are the old problems still there?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;goals&quot;&gt;Goals&lt;&#x2F;h1&gt;
&lt;p&gt;What do I want from the client-server API:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cheap on the wire&lt;&#x2F;strong&gt; - less data and CPU, which on mobile means cellular data and battery.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A strict shared contract&lt;&#x2F;strong&gt; - one source of truth both ends are generated from, and tests are ran against.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Native bidirectional streaming&lt;&#x2F;strong&gt; - real-time sync without bolting on a second protocol.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Works on every client&lt;&#x2F;strong&gt; - mobile, desktop, &lt;em&gt;and&lt;&#x2F;em&gt; web.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Minimal extra infra&lt;&#x2F;strong&gt; - no transcoder, no separate streaming server&#x2F;load balancer.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;the-options&quot;&gt;The options&lt;&#x2F;h1&gt;
&lt;p&gt;The current set of available options seems to be:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Translate to REST&lt;&#x2F;strong&gt; - &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;grpc-ecosystem&#x2F;grpc-gateway&quot;&gt;gRPC-gateway&lt;&#x2F;a&gt; or &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.envoyproxy.io&#x2F;&quot;&gt;Envoy&lt;&#x2F;a&gt; generate a REST API from the &lt;code&gt;.proto&lt;&#x2F;code&gt; specification. The conventional default. &lt;em&gt;Both actively maintained.&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;grpc.io&quot;&gt;gRPC&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; + &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;grpc&#x2F;grpc-web&quot;&gt;gRPC-Web&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - gRPC on the wire: native HTTP&#x2F;2 + protobuf for mobile&#x2F;desktop, gRPC-Web (re-encoded, via a proxy such as &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.envoyproxy.io&#x2F;&quot;&gt;Envoy&lt;&#x2F;a&gt;) for browsers. &lt;em&gt;Core gRPC is active; gRPC-Web is in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;grpc&#x2F;grpc-web&#x2F;blob&#x2F;master&#x2F;doc&#x2F;roadmap.md&quot;&gt;maintenance mode&lt;&#x2F;a&gt;.&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;connectrpc.com&quot;&gt;Connect&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - mostly gRPC&#x27;s efficiency (binary protobuf over HTTP&#x2F;2) but reshaped to talk HTTP with some quality a better observability. Native clients reach gRPC directly; web still needs a Connect - gRPC bridge (Envoy or a Connect edge), transcoding is lighter than from JSON though. &lt;em&gt;Actively maintained.&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;why-even-consider-grpc&quot;&gt;Why even consider gRPC&lt;&#x2F;h1&gt;
&lt;p&gt;gRPC being the more capable protocol isn&#x27;t really controversial:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Performance:&lt;&#x2F;strong&gt; less data on the wire, fewer CPU cycles serializing, one reused connection - less latency and battery drain.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Contract-first:&lt;&#x2F;strong&gt; the &lt;code&gt;.proto&lt;&#x2F;code&gt; always comes first by design. REST&#x27;s OpenAPI codegen isn&#x27;t that great, so you end up implementing API first and documenting it later which is never done. With gRPC spec tests and server mocks are free.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Streaming:&lt;&#x2F;strong&gt; native, bidirectional, same stack and types. REST streams one way at best (chunked&#x2F;SSE); bidirectional needs a second protocol (WebSocket), often its own server and load balancer.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Flow control:&lt;&#x2F;strong&gt; cancellation, deadlines, backpressure built in.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So is it realistic to gain all these benefits in a multiplatform app in 2026?&lt;&#x2F;p&gt;
&lt;h1 id=&quot;option-1-translate-to-rest&quot;&gt;Option 1: Translate to REST&lt;&#x2F;h1&gt;
&lt;p&gt;The conventional default, and what my transcoder does today. It&#x27;s nearly free if Envoy is already your edge - the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.envoyproxy.io&#x2F;docs&#x2F;envoy&#x2F;latest&#x2F;configuration&#x2F;http&#x2F;http_filters&#x2F;grpc_json_transcoder_filter&quot;&gt;gRPC-JSON transcoder&lt;&#x2F;a&gt; is just a filter. &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;grpc-ecosystem&#x2F;grpc-gateway&quot;&gt;gRPC-gateway&lt;&#x2F;a&gt; does the same as a standalone proxy behind any ingress, at the cost of one more service to run. The control group we compare to.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Both &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;grpc-ecosystem&#x2F;grpc-gateway&quot;&gt;gRPC-gateway&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.envoyproxy.io&#x2F;&quot;&gt;Envoy&lt;&#x2F;a&gt; are actively maintained.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;option-2-grpc-grpc-web&quot;&gt;Option 2: gRPC + gRPC-Web&lt;&#x2F;h1&gt;
&lt;p&gt;Here you can get all the benefits, but it very much depends on the client platform.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;native-mobile-desktop&quot;&gt;Native (mobile, desktop)&lt;&#x2F;h2&gt;
&lt;p&gt;Full gRPC gets you all the benefits mentioned above. If you don&#x27;t have web clients - the choice is clear, or is it?&lt;&#x2F;p&gt;
&lt;p&gt;Their client lives in uncontrolled environment, meaning things like CGNAT, TLS inspection proxies, and corporate DPI firewalls.&lt;&#x2F;p&gt;
&lt;p&gt;CGNAT seems not to be an issue, except for dropping a connection from time to time, especially if it sits idle, but first of all this is not an unrecoverable error, and secondly, and most importantly, this issue affects any kind of streaming, including the ones REST API would rely on, namely web-sockets and SSEs.&lt;&#x2F;p&gt;
&lt;p&gt;Majority of firewalls and proxies also shouldn&#x27;t be an issue, since traffic is HTTP&#x2F;2 encrypted with TLS - it doesn&#x27;t look too different from a web socket connection. What is an issue is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;TLS_termination_proxy&quot;&gt;TLS terminating proxies&lt;&#x2F;a&gt;, if they downgrade the &#x27;internal&#x27; leg of their connection to HTTP&#x2F;1.1. Though I am yet to encounter one in the wild, and don&#x27;t expect my users to be the ones who would.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;web&quot;&gt;Web&lt;&#x2F;h2&gt;
&lt;p&gt;Browsers can&#x27;t speak native gRPC simply because they don&#x27;t expose the API specific to any given HTTP version, instead they want web developers not to care what generation of HTTP is used, so it is abstracted out. This is great for the REST, but gRPC needs control over individual HTTP&#x2F;2 frames, which makes it not a viable option for browser usage as is.&lt;&#x2F;p&gt;
&lt;p&gt;Couple of years ago Google was pushing gRPC-web as a way of covering the gap. The idea behind it was that gRPC is carried, with minimal changes, over a plain HTTP request, with a proxy such as Envoy translating it back to gRPC. You can see this even in Dart&#x27;s gRPC package structure: the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pub.dev&#x2F;packages&#x2F;grpc&quot;&gt;&lt;code&gt;grpc&lt;&#x2F;code&gt; package&lt;&#x2F;a&gt; doesn&#x27;t list web as a supported platform, yet ships a separate &lt;code&gt;grpc_web.dart&lt;&#x2F;code&gt; library just for the browser. That is why.&lt;&#x2F;p&gt;
&lt;p&gt;It has a bunch of limitations, such as limited streaming capabilities, no flow control, limited cancellation, CORS preflight penalties, but what is more important is that it is now in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;grpc&#x2F;grpc-web&#x2F;blob&#x2F;master&#x2F;doc&#x2F;roadmap.md&quot;&gt;maintenance mode&lt;&#x2F;a&gt;. Because it was built on two of Google&#x27;s own libraries - that have since been archived, maintaining the protocol became too cumbersome. The current official recommendation is to use gRPC-Gateway, which brings us back to &lt;a href=&quot;https:&#x2F;&#x2F;shtein.me&#x2F;posts&#x2F;rest-vs-grpc-for-public-api&#x2F;#option-1-translate-to-rest&quot;&gt;option 1&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;other-downsides&quot;&gt;Other downsides&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debugging &amp;amp; tooling:&lt;&#x2F;strong&gt; while tools like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postman.com&#x2F;&quot;&gt;Postman&lt;&#x2F;a&gt; or &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.usebruno.com&#x2F;&quot;&gt;Bruno&lt;&#x2F;a&gt; do support gRPC, and there is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;fullstorydev&#x2F;grpcurl&quot;&gt;grpcurl&lt;&#x2F;a&gt; to replace curl for the CLI interactions - you still lose ability to glance at traffic in your dev tools or request logs, which does reduce the debuggability of the set up.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Caching &amp;amp; CDNs:&lt;&#x2F;strong&gt; since every call is a POST on a long-lived connection - cache goes out of the window basically, so for a read heavy application - you may even lose in terms of speed.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Load balancing:&lt;&#x2F;strong&gt; a non-issue in practice - a public REST API already sits behind an L7 reverse proxy, the only extra requirement is that it be gRPC-aware, which all the modern ones are.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;HTTP semantics:&lt;&#x2F;strong&gt; your monitoring tool should be aware of, for example, gRPC error codes. You can&#x27;t anymore rely on error detection based on HTTP status codes.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;em&gt;Core gRPC is actively maintained; gRPC-Web is in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;grpc&#x2F;grpc-web&#x2F;blob&#x2F;master&#x2F;doc&#x2F;roadmap.md&quot;&gt;maintenance mode&lt;&#x2F;a&gt;.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;option-3-connect&quot;&gt;Option 3: Connect&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;connectrpc.com&quot;&gt;Connect&lt;&#x2F;a&gt; is an attempt to fix most of the issues gRPC presents while keeping the efficiency. It&#x27;s a new protocol that is designed to be fully compatible with generic HTTP, which resolves a lot of the downsides gRPC&#x27;s HTTP&#x2F;2 reliance brings.&lt;&#x2F;p&gt;
&lt;p&gt;Pros:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Stub generation from &lt;code&gt;.proto&lt;&#x2F;code&gt; specification&lt;&#x2F;li&gt;
&lt;li&gt;Binary protobuf payloads&lt;&#x2F;li&gt;
&lt;li&gt;Works over any version of HTTP, including 1.1, which makes it:
&lt;ul&gt;
&lt;li&gt;Browser native&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;curl&lt;&#x2F;code&gt;-able&lt;&#x2F;li&gt;
&lt;li&gt;Cacheable for calls with no side effects, since they can use GET&lt;&#x2F;li&gt;
&lt;li&gt;Error detectable, since they now live in the HTTP response&lt;&#x2F;li&gt;
&lt;li&gt;Removes the need of interop layer on the client&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Important for my case it has &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pub.dev&#x2F;packages&#x2F;connectrpc&quot;&gt;official dart package&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Cons:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;You still need a translation layer, though less intensive one compared to REST since it is just repackaging of a packet rather than full remarshalling&lt;&#x2F;li&gt;
&lt;li&gt;The protocol is much less adopted, so less tooling, less support&lt;&#x2F;li&gt;
&lt;li&gt;Streaming is still limited on web, mostly individual requests are used&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;em&gt;Actively maintained by CNCF.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Capability&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;Translate to REST&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;gRPC + gRPC-Web&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;Connect&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Web support&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅ via proxy&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Low bandwidth (client)&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;❌ JSON&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Bidir streaming&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;❌&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅ native &#x2F; ❌ web&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅ native &#x2F; ❌ web&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Caching &#x2F; CDN&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;❌&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Tooling &#x2F; debugging&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;⚠️ binary&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;⚠️ binary &#x2F; ✅ JSON&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Monitoring on HTTP status&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;❌&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Network robustness&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;⚠️ native needs e2e HTTP&#x2F;2&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Strict client contract&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;⚠️ OpenAPI&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅ proto&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅ proto&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Dart support&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Edge translation&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;heavy transcoder&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;proxy (web)&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;light bridge (web)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Maintenance&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;⚠️ gRPC-Web in maintenance&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Ecosystem &#x2F; adoption&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅ huge&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅ mature&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;⚠️ young&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;the-conclusion&quot;&gt;The conclusion&lt;&#x2F;h1&gt;
&lt;p&gt;So is gRPC viable in 2026?&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;d say no. Large projects, the ones that actually may benefit from such a decision, are very dependent on caching, on having clear visibility into the data, and on supporting as many clients as possible - which basically rules out gRPC for them,
in both web and non-web forms. The fact that gRPC-Web is in maintenance mode makes this ruling even simpler.&lt;&#x2F;p&gt;
&lt;p&gt;Connect, on the other hand, is tempting. I came into writing this post thinking I&#x27;d choose it for my project, for research purposes if nothing else, but looking at the picture we have here - it&#x27;s still a bunch of complexity for a fairly modest
gain. So I&#x27;d say it still shouldn&#x27;t be a choice, unless both performance and robustness are of the highest priority.&lt;&#x2F;p&gt;
&lt;p&gt;To conclude: gRPC to REST conversion still should be the default. If you need to keep the amount of data between client and server to a minimum, or you need the most performant streaming you can get, and reliability is not an issue - gRPC is the choice.
If you need it to be reliable too, though - go with Connect.&lt;&#x2F;p&gt;
&lt;p&gt;I will continue with the REST API for my project.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Beyond the Empty Build: Evolving Riverpod Architecture</title>
        <published>2026-01-17T15:47:00+07:00</published>
        <updated>2026-01-17T15:47:00+07:00</updated>
        
        <author>
          <name>
            
              Igor Shtein
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shtein.me/posts/empty-riverpod-notifier-build-method/"/>
        <id>https://shtein.me/posts/empty-riverpod-notifier-build-method/</id>
        
        <content type="html" xml:base="https://shtein.me/posts/empty-riverpod-notifier-build-method/">&lt;p&gt;After developing for some time with &lt;a href=&quot;&#x2F;tags&#x2F;bloc&quot;&gt;BLoC&lt;&#x2F;a&gt;, I finally made the switch to &lt;a href=&quot;&#x2F;tags&#x2F;riverpod&quot;&gt;Riverpod&lt;&#x2F;a&gt;. The benefits looked really clear as I saw Riverpod as an evolution of BLoC that goes beyond the abstractions of business logic and actually handles orchestration of the logic blocks. However, with this orchestration comes complexity, and not complexity of riverpod itself, but from the fact that you now can do the same thing in multiple ways, and it isn&#x27;t always obvious which path is the &quot;right&quot; one.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;codewithandrea.com&#x2F;&quot;&gt;Code with Andrea&lt;&#x2F;a&gt; blog was a godsend during my initial search for a solid architecture. The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;codewithandrea.com&#x2F;articles&#x2F;flutter-app-architecture-riverpod-introduction&#x2F;&quot;&gt;system he came up with&lt;&#x2F;a&gt; covers everything, but some things didn&#x27;t quite sit right with me. One such thing that immediately felt off was the &lt;strong&gt;empty &lt;code&gt;build&lt;&#x2F;code&gt; method&lt;&#x2F;strong&gt; in notifiers acting as &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;codewithandrea.com&#x2F;articles&#x2F;flutter-presentation-layer&#x2F;#a-controller-class-based-on-asyncnotifier&quot;&gt;view controllers&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-empty-build-pattern&quot;&gt;The &quot;Empty Build&quot; Pattern&lt;&#x2F;h3&gt;
&lt;p&gt;It may actually be the best way of handling a side effect&#x27;s (like signing in or submitting a form) state as opposed to the view state itself in the Riverpod 2.x, using an &lt;code&gt;AsyncNotifier&amp;lt;void&amp;gt;&lt;&#x2F;code&gt;. It looks like this:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;dart&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;@riverpod&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt; SignInScreenController&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt; extends&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt; _$SignInScreenController&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;  @override&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;  FutureOr&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;void&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt; build&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#008000, #6A9955);&quot;&gt;    &#x2F;&#x2F; no-op: This controller provides no initial data.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;  Future&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;void&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt; signInAnonymously&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt;async&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    state &lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt; const&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt; AsyncLoading&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    state &lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt; await&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt; AsyncValue&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt;guard&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;() &lt;&#x2F;span&gt;&lt;span&gt;=&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; ref&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt;read&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;authRepositoryProvider)&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt;signInAnonymously&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I think that at the time, this was a perfectly valid way to handle UI state. Thanks to Riverpod’s use of result wrapper to indicate if a side effect is ongoing or failed. But when migrating my logic from BLoCs I couldn&#x27;t get rid of feeling that the state has to be managed centrally without splitting the responsibility.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-problem-fragmented-responsibility&quot;&gt;The Problem: Fragmented Responsibility&lt;&#x2F;h3&gt;
&lt;p&gt;The empty &lt;code&gt;build&lt;&#x2F;code&gt; method essentially exports the state of the notifier back into the UI. Because the controller is &quot;stateless&quot; (returning &lt;code&gt;void&lt;&#x2F;code&gt;), it doesn&#x27;t actually hold the data the view needs. It only holds the &lt;em&gt;status&lt;&#x2F;em&gt; of the last action.&lt;&#x2F;p&gt;
&lt;p&gt;This forces the UI to manage a split personality:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;It watches the &lt;strong&gt;Controller&lt;&#x2F;strong&gt; to see if a button should show a spinner.&lt;&#x2F;li&gt;
&lt;li&gt;It watches a &lt;strong&gt;different Provider&lt;&#x2F;strong&gt; elsewhere to get the actual data.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Instead of gracefully handling state transitions in a single place, the controller triggers an effect, updates a separate provider, and leaves the UI to interpret the result. This makes the UI a middleman for logic that the Notifier is perfectly capable of handling itself.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;a-code-comparison-split-vs-concentrated&quot;&gt;A Code Comparison: Split vs. Concentrated&lt;&#x2F;h3&gt;
&lt;p&gt;To see why this matters, let’s look at how we update a user&#x27;s profile.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-the-split-state-empty-build&quot;&gt;1. The Split State (Empty Build)&lt;&#x2F;h4&gt;
&lt;p&gt;Here, the logic is fragmented. You have to manually keep two providers in sync.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;dart&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;FutureOr&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;void&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt; build&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;) {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;mermaid&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;graph TD&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    subgraph UI [&amp;quot;Widget Tree&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        W[View &#x2F; Widget]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    subgraph Logic [&amp;quot;Riverpod Providers&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        DP[Data Provider]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        Ctrl[Controller]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    DB[(Database)]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    W --&amp;gt;|1. Call Action| Ctrl&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Ctrl --&amp;gt;|2. State = Loading| W&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Ctrl --&amp;gt;|3. Update DB| DB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    DB -.-&amp;gt;|4. Emit Event| DP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    DP --&amp;gt;|5. Trigger Rebuild| W&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    DP -.-&amp;gt;|5. Trigger Rebuild| Ctrl&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style Ctrl fill:#f96,stroke:#333&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style DP fill:#bbf,stroke:#333&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h4 id=&quot;2-the-concentrated-state-the-bloc-like-way&quot;&gt;2. The Concentrated State (The BLoC-like Way)&lt;&#x2F;h4&gt;
&lt;p&gt;By returning the actual data in the &lt;code&gt;build&lt;&#x2F;code&gt; method, the Notifier becomes the single source of truth.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;dart&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;FutureOr&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;User&lt;&#x2F;span&gt;&lt;span&gt;?&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt; build&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span&gt;=&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; userProvider&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;future&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;mermaid&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;graph TD&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    subgraph UI [&amp;quot;Widget Tree&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        W[View &#x2F; Widget]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    subgraph Logic [&amp;quot;Single Source of Truth&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        N[User Notifier]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    DB[(Database)]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    W --&amp;gt;|1. Call Action| N&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    N --&amp;gt;|2. State = Loading| W&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    N --&amp;gt;|3. Update DB| DB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    DB --&amp;gt;|4. Return Result| N&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    N --&amp;gt;|5. State = Success + New Data| W&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style N fill:#dfd,stroke:#333&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;the-better-solution-riverpod-3-mutations&quot;&gt;The Better Solution: Riverpod 3 Mutations&lt;&#x2F;h3&gt;
&lt;p&gt;While the concentrated approach is cleaner, it has a downside: if the state is &lt;code&gt;AsyncLoading&lt;&#x2F;code&gt;, the entire UI usually replaces the data with a spinner. What if you want to keep showing the old data while the &quot;Save&quot; button spins?&lt;&#x2F;p&gt;
&lt;p&gt;This is where Remi, author of &lt;a href=&quot;&#x2F;tags&#x2F;riverpod&quot;&gt;Riverpod&lt;&#x2F;a&gt; got us covered with &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;riverpod.dev&#x2F;docs&#x2F;concepts2&#x2F;mutations&quot;&gt;Mutations&lt;&#x2F;a&gt; in version 3 solve this by separating the &lt;strong&gt;Application State&lt;&#x2F;strong&gt; (data) from the &lt;strong&gt;Transaction State&lt;&#x2F;strong&gt; (the side effect).&lt;&#x2F;p&gt;
&lt;p&gt;Instead of a &quot;Void Controller,&quot; you define a mutation:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;dart&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#008000, #6A9955);&quot;&gt;&#x2F;&#x2F; A standalone mutation to track the &amp;quot;Save&amp;quot; action&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;final&lt;&#x2F;span&gt;&lt;span&gt; updateNameMutation &lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt; Mutation&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;void&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#008000, #6A9955);&quot;&gt;&#x2F;&#x2F; UI Usage:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;onPressed&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span&gt; () {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  updateNameMutation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt;run&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;ref&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span&gt; (tsx) &lt;&#x2F;span&gt;&lt;span&gt;=&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; ref&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt;read&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;userNotifierProvider&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;notifier)&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt;updateName&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;name))&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#008000, #6A9955);&quot;&gt;&#x2F;&#x2F; Watch &amp;#39;updateNameMutation&amp;#39; for the button spinner, &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#008000, #6A9955);&quot;&gt;&#x2F;&#x2F; while &amp;#39;userNotifierProvider&amp;#39; continues to show the user data.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h3&gt;
&lt;p&gt;The &quot;Empty Build&quot; method was a necessary workaround in Riverpod 2.x for tracking side effects without a native tool. It was a bridge that helped us move away from manual &lt;code&gt;setState&lt;&#x2F;code&gt; logic in our widgets.&lt;&#x2F;p&gt;
&lt;p&gt;However, with the arrival of &lt;strong&gt;Riverpod 3&lt;&#x2F;strong&gt;, we can return to a more robust architecture. By using &lt;strong&gt;Mutations&lt;&#x2F;strong&gt; for side effects and keeping our &lt;strong&gt;Notifiers&lt;&#x2F;strong&gt; focused on holding data, we stop exporting state responsibility to the view. We can build upon the foundations laid by developers like Andrea while utilizing these new developments to keep our controllers meaningful and our UI predictable.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Should dart generated files be commited into VCS</title>
        <published>2026-01-17T14:57:00+07:00</published>
        <updated>2026-01-17T14:57:00+07:00</updated>
        
        <author>
          <name>
            
              Igor Shtein
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shtein.me/posts/dart-generated-files-in-vcs/"/>
        <id>https://shtein.me/posts/dart-generated-files-in-vcs/</id>
        
        <content type="html" xml:base="https://shtein.me/posts/dart-generated-files-in-vcs/">&lt;p&gt;So you’ve installed &lt;a href=&quot;&#x2F;tags&#x2F;riverpod&quot;&gt;Riverpod&lt;&#x2F;a&gt;, &lt;a href=&quot;&#x2F;tags&#x2F;freezed&#x2F;&quot;&gt;Freezed&lt;&#x2F;a&gt;, or &lt;a href=&quot;&#x2F;tags&#x2F;drift&quot;&gt;Drift&lt;&#x2F;a&gt;, ran the code generation for the first time, and noticed a bunch of new files with &lt;code&gt;.g.dart&lt;&#x2F;code&gt; or &lt;code&gt;.freezed.dart&lt;&#x2F;code&gt; appearing. Now you’re wondering: &lt;strong&gt;Should I commit these files to version control?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Unfortunately, there is no &quot;one size fits all&quot; answer. The short version is: &lt;strong&gt;If you aren&#x27;t sure, you should commit them.&lt;&#x2F;strong&gt; However, there are specific circumstances where you might choose not to.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;why-dart-is-different&quot;&gt;Why Dart is Different&lt;&#x2F;h4&gt;
&lt;p&gt;Unlike some languages that generate hidden bytecode or binary files (which should always be ignored), &lt;a href=&quot;&#x2F;tags&#x2F;dart&quot;&gt;Dart&lt;&#x2F;a&gt; code generators produce actual &lt;code&gt;.dart&lt;&#x2F;code&gt; source code that your application relies on to compile. For your app to function, these files must exist. If you choose not to commit them, either your CI&#x2F;CD pipeline or every developer who pulls your code will have to generate them manually.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;the-case-for-committing-the-default-path&quot;&gt;The Case for Committing (The Default Path)&lt;&#x2F;h4&gt;
&lt;p&gt;If you are building a &lt;strong&gt;Library&lt;&#x2F;strong&gt;, you must include the generated code; otherwise, users cannot use your package. For &lt;strong&gt;Applications&lt;&#x2F;strong&gt;, committing is generally preferred for several reasons:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reliability:&lt;&#x2F;strong&gt; It prevents &quot;bugs&quot; caused by version mismatches. If the build machine has a different version of &lt;a href=&quot;&#x2F;tags&#x2F;flutter&quot;&gt;Flutter&lt;&#x2F;a&gt; or &lt;code&gt;build_runner&lt;&#x2F;code&gt; than your local machine, the generated code might differ, leading to unexpected behavior.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Speed:&lt;&#x2F;strong&gt; Generating code makes pipelines slower and more expensive. Committing the files allows your CI&#x2F;CD to start testing and building immediately.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Environment Consistency:&lt;&#x2F;strong&gt; Some packages, like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pub.dev&#x2F;packages&#x2F;envied&quot;&gt;Envied&lt;&#x2F;a&gt;, generate code based on environment variables. Committing these ensures that the code you tested is exactly what gets deployed.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;the-case-for-ignoring-the-exception&quot;&gt;The Case for Ignoring (The Exception)&lt;&#x2F;h4&gt;
&lt;p&gt;There are times when you may want to exclude these files from Git or force the pipeline to regenerate them:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Security:&lt;&#x2F;strong&gt; If you use packages like Envied to inject production credentials via environment variables during the build process, you definitely want to regenerate that code in your secure pipeline rather than storing it in Git.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Repo Health:&lt;&#x2F;strong&gt; Generated files can &quot;bloat&quot; your &lt;a href=&quot;&#x2F;tags&#x2F;vcs&quot;&gt;VCS&lt;&#x2F;a&gt; history.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Merge Conflicts:&lt;&#x2F;strong&gt; If two developers modify the same model, the generated files will often result in complex merge conflicts that cannot be resolved manually.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PR Noise:&lt;&#x2F;strong&gt; Generated code adds thousands of lines to Pull Requests, making it harder for reviewers to focus on the logic that actually matters.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h4&gt;
&lt;p&gt;As a general rule, &lt;strong&gt;commit your generated code&lt;&#x2F;strong&gt; unless you specifically need your pipeline to build a different version of the code (like environment-specific secrets) than what the developers use locally. If the &quot;PR noise&quot; bothers you, look into using a &lt;code&gt;.gitattributes&lt;&#x2F;code&gt; file to mark generated files as linguist-generated, which collapses them automatically on platforms like GitHub.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Hiding generated flutter files in Android Studio</title>
        <published>2025-10-20T10:00:00+07:00</published>
        <updated>2026-01-17T14:57:00+07:00</updated>
        
        <author>
          <name>
            
              Igor Shtein
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shtein.me/posts/dart-generated-files-management/"/>
        <id>https://shtein.me/posts/dart-generated-files-management/</id>
        
        <content type="html" xml:base="https://shtein.me/posts/dart-generated-files-management/">&lt;p&gt;Say you are following &lt;a href=&quot;&#x2F;tags&#x2F;flutter&quot;&gt;Flutter&lt;&#x2F;a&gt; best practices using immutable models, and historically the best way of doing it have been the &lt;a href=&quot;&#x2F;tags&#x2F;freezed&quot;&gt;freezed&lt;&#x2F;a&gt; package.&lt;&#x2F;p&gt;
&lt;p&gt;If you do - you are likely annoyed with all the files generated by it where you end up with 3 files instead of 1. Excluding these files from &lt;a href=&quot;&#x2F;tags&#x2F;android-studio&quot;&gt;IDE&lt;&#x2F;a&gt; altogether is not practical since you may want to pick into the generated code to see what it actually looks like, especially in case of &lt;a href=&quot;&#x2F;tags&#x2F;riverpod&quot;&gt;providers&lt;&#x2F;a&gt;. So is there a way to make these files less annoying when you don&#x27;t need them? Yes! You can hide them from the project file tree while keeping them accessible when needed, it looks like so:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;shtein.me&#x2F;posts&#x2F;dart-generated-files-management&#x2F;img&#x2F;collapsable_files.png&quot; alt=&quot;collapsable_files&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;It is very easy to enable this. Go to Project settings -&amp;gt; Appearance -&amp;gt; File nesting&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;shtein.me&#x2F;posts&#x2F;dart-generated-files-management&#x2F;img&#x2F;menu_navigation.png&quot; alt=&quot;file_nesting_menu&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;And add file extensions of generated files. &lt;code&gt;.freezed.dart&lt;&#x2F;code&gt; and &lt;code&gt;.g.dart&lt;&#x2F;code&gt; in our case.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;shtein.me&#x2F;posts&#x2F;dart-generated-files-management&#x2F;img&#x2F;add_extensions.png&quot; alt=&quot;file_nesting_extensions_add&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s it. Simple.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>N+1 query problem in Django ORM</title>
        <published>2025-08-16T15:23:51+07:00</published>
        <updated>2025-08-16T15:23:51+07:00</updated>
        
        <author>
          <name>
            
              Igor Shtein
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shtein.me/posts/n-plus-one-query-problem/"/>
        <id>https://shtein.me/posts/n-plus-one-query-problem/</id>
        
        <content type="html" xml:base="https://shtein.me/posts/n-plus-one-query-problem/">&lt;h2 id=&quot;the-n-1-query-problem-a-django-developer-s-nightmare-and-how-to-wake-up&quot;&gt;The N+1 Query Problem: A Django Developer&#x27;s Nightmare (And How to Wake Up)&lt;&#x2F;h2&gt;
&lt;p&gt;Picture this: Your Django application has been humming along beautifully for months. Users are happy, servers are stable, and you&#x27;re feeling pretty good about your architectural choices. Then you casually open &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;sentry.io&quot;&gt;Sentry&lt;&#x2F;a&gt; for your morning dose of masochism, and BAM—a new issue appears like an unwelcome party crasher: &quot;N+1 Queries Detected.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;The timestamp? Right when you deployed that shiny new &quot;user groups&quot; feature you spent weeks perfecting. You click through to the session replay, watch in horror as your once-zippy request crawls along like it&#x27;s stuck in digital molasses, and think: &quot;What fresh hell is this?&quot;&lt;&#x2F;p&gt;
&lt;p&gt;Welcome to the N+1 query problem—Django&#x27;s favorite way of reminding developers that laziness isn&#x27;t always a virtue.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-problem-or-how-your-orm-became-a-database-interrogator&quot;&gt;The Problem (Or: How Your ORM Became a Database Interrogator)&lt;&#x2F;h3&gt;
&lt;p&gt;The N+1 query problem is like that friend who asks &quot;just one more question&quot; after every answer you give. Here&#x27;s what happens: your application loads a list of &lt;em&gt;N&lt;&#x2F;em&gt; items, then proceeds to make &lt;em&gt;1 additional&lt;&#x2F;em&gt; query for each item in that list. Instead of the single, efficient database conversation you expected, you end up with N+1 separate database round trips.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s like going to the grocery store and making a separate trip for each item on your list. Technically it works, but your database administrator is definitely judging you.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-does-this-happen-the-perfect-storm&quot;&gt;Why Does This Happen? (The Perfect Storm)&lt;&#x2F;h3&gt;
&lt;p&gt;The N+1 problem requires just three ingredients to create the perfect storm:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A relational database&lt;&#x2F;strong&gt; (check—you&#x27;re using PostgreSQL&#x2F;MySQL&#x2F;SQLite)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;An ORM with lazy loading&lt;&#x2F;strong&gt; (double check—Django ORM loves being lazy)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A list of items to iterate over&lt;&#x2F;strong&gt; (triple check—what&#x27;s an app without lists?)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Since Django ORM is lazy by default (and who can blame it?), encountering this problem isn&#x27;t a matter of &quot;if&quot; but &quot;when.&quot; It&#x27;s practically a rite of passage for Django developers.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;a-classic-example-starring-books-and-authors&quot;&gt;A Classic Example (Starring Books and Authors)&lt;&#x2F;h3&gt;
&lt;p&gt;Let&#x27;s say you have two models that would make any literature professor proud:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt; Author&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;models&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;Model&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; models&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;CharField&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#001080, #9CDCFE);&quot;&gt;max_length&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#098658, #B5CEA8);&quot;&gt;100&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt; Book&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;models&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;Model&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    title&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; models&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;CharField&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#001080, #9CDCFE);&quot;&gt;max_length&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#098658, #B5CEA8);&quot;&gt;100&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    author&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; models&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;ForeignKey&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;Author&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#001080, #9CDCFE);&quot;&gt; on_delete&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span&gt;models&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;CASCADE&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You want to display a simple list of books with their authors. Piece of cake, right?&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;books&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Book&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;objects&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;all&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt;for&lt;&#x2F;span&gt;&lt;span&gt; book&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt; in&lt;&#x2F;span&gt;&lt;span&gt; books&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt;    print&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;book&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;title&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span&gt; book&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;author&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;name&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Congratulations! You&#x27;ve just created a database query monster. Each time you access &lt;code&gt;book.author.name&lt;&#x2F;code&gt;, Django&#x27;s lazy ORM thinks, &quot;Oh, you need that author? Let me make a quick database trip for you.&quot; Multiply that by however many books you have, and you&#x27;ve got yourself a performance nightmare.&lt;&#x2F;p&gt;
&lt;p&gt;If you have 100 books, you&#x27;re making 101 queries (1 for the books + 100 for the authors). Your database is working overtime while your users are wondering if their internet connection died.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-solution-or-how-to-make-your-orm-stop-being-lazy&quot;&gt;The Solution (Or: How to Make Your ORM Stop Being Lazy)&lt;&#x2F;h3&gt;
&lt;p&gt;The fix is beautifully simple and follows the &quot;work smarter, not harder&quot; principle: if you know you&#x27;ll need related data, fetch it all upfront instead of playing database ping-pong.&lt;&#x2F;p&gt;
&lt;p&gt;Django makes this almost embarrassingly easy with &lt;code&gt;select_related&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;books&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Book&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;objects&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;select_related&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;author&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;all&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt;for&lt;&#x2F;span&gt;&lt;span&gt; book&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt; in&lt;&#x2F;span&gt;&lt;span&gt; books&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#795E26, #DCDCAA);&quot;&gt;    print&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;book&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;title&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span&gt; book&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;author&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;name&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That&#x27;s it. One line change, and you&#x27;ve gone from 101 queries to exactly 1. Your database can finally take a breath, your users get their pages instantly, and you get to keep your job.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-real-challenge-detection-and-prevention&quot;&gt;The Real Challenge (Detection and Prevention)&lt;&#x2F;h3&gt;
&lt;p&gt;Here&#x27;s the catch: spotting N+1 queries before they hit production is trickier than solving them. You need a decent amount of test data and proper monitoring to catch these performance gremlins. In development with your 5-record test database, everything feels lightning fast.&lt;&#x2F;p&gt;
&lt;p&gt;The sneaky part is that relationship access might happen far from where you created the queryset—like in a template that&#x27;s three layers deep in your rendering stack. By the time you realize what&#x27;s happening, your users are already experiencing the slowdown.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;your-monitoring-arsenal&quot;&gt;Your Monitoring Arsenal&lt;&#x2F;h3&gt;
&lt;p&gt;Modern APM systems like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;newrelic.com&#x2F;&quot;&gt;New Relic&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;sentry.io&#x2F;&quot;&gt;Sentry&lt;&#x2F;a&gt; have gotten pretty good at detecting N+1 patterns automatically. However, they typically rely on thresholds (number of queries, execution time) to flag issues, which means they&#x27;ll only alert you after the problem has already affected your users.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Pro tip&lt;&#x2F;strong&gt;: Adjust your APM sensitivity settings to catch these issues earlier. Your future self (and your users) will thank you.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-bottom-line&quot;&gt;The Bottom Line&lt;&#x2F;h3&gt;
&lt;p&gt;The N+1 query problem is like that recurring villain in superhero movies—it&#x27;ll keep showing up until you learn to defeat it properly. The good news? Once you understand the pattern and Django&#x27;s solutions like &lt;code&gt;select_related&lt;&#x2F;code&gt; and &lt;code&gt;prefetch_related&lt;&#x2F;code&gt;, you&#x27;ll start spotting potential issues during code reviews and catch them before they escape into the wild.&lt;&#x2F;p&gt;
&lt;p&gt;Remember: in the world of database queries, being lazy upfront often means working harder later. Sometimes the best performance optimization is simply asking for what you need, when you need it, all at once.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Breaking Django ORM migrations</title>
        <published>2024-03-28T14:13:00-07:00</published>
        <updated>2024-03-28T14:13:00-07:00</updated>
        
        <author>
          <name>
            
              Igor Shtein
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shtein.me/posts/blue-green-deployment-django-orm/"/>
        <id>https://shtein.me/posts/blue-green-deployment-django-orm/</id>
        
        <content type="html" xml:base="https://shtein.me/posts/blue-green-deployment-django-orm/">&lt;h2 id=&quot;backward-incompatible-migrations&quot;&gt;Backward incompatible migrations&lt;&#x2F;h2&gt;
&lt;p&gt;Some migrations may be backward incompatible and will break production for a shot period of time between the migration application and pod rotation.
This also will be a problem for a blue&#x2F;green deployment if you to decide to implement it.
To avoid this kind of downtime migration should be done in 2 steps.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;caution&quot;&gt;Caution&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Use this method only for the listed below operations. This method is dangerous. It can dissync the state of the app from DB. Never use self written SQL. Always copy it from the sqlmigrate command.&lt;&#x2F;li&gt;
&lt;li&gt;This type of migration is hard to revert for obvious reasons, so be extra careful doing it.&lt;&#x2F;li&gt;
&lt;li&gt;Doing this operation without using &lt;code&gt;SeparateDatabaseAndState&lt;&#x2F;code&gt; will result into a downtime. Workers will refuse to start if change is made to the model, but not applied to the django internal state.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;list-of-breaking-migration-operations&quot;&gt;List of breaking migration operations&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RemoveField&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;DeleteModel&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;AddField&lt;&#x2F;code&gt; when the field is not nullable (there is no &lt;code&gt;null=True&lt;&#x2F;code&gt; in the field declaration)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;solution&quot;&gt;Solution&lt;&#x2F;h3&gt;
&lt;p&gt;Create 2 PR&#x27;s and merge them into 2 separate releases:&lt;&#x2F;p&gt;
&lt;h4 id=&quot;usages-removal-pr&quot;&gt;Usages removal PR&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;Remove all the usages of the field from the code&lt;&#x2F;li&gt;
&lt;li&gt;Remove the field from the model&lt;&#x2F;li&gt;
&lt;li&gt;Autogenerate a migration (&lt;code&gt;.&#x2F;manage.py makemigrations your_app&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;Retrieve the sql of the migration (we&#x27;ll need it later) (&lt;code&gt;.&#x2F;manage.py sqlmigrate your_app 0002_auto_20240328_1527&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;Move the breaking operations to &lt;code&gt;SeparateDatabaseAndState&lt;&#x2F;code&gt;s &lt;code&gt;state_operations&lt;&#x2F;code&gt; (example below).&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;database-changes-pr&quot;&gt;Database changes PR&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;Create empty migration (&lt;code&gt;.&#x2F;manage.py makemigrations your_app --empty&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;Add &lt;code&gt;SeparateDatabaseAndState&lt;&#x2F;code&gt; again&lt;&#x2F;li&gt;
&lt;li&gt;Put the SQL extracted in step 4 of previous PR creation into &lt;code&gt;database_operations&lt;&#x2F;code&gt;. Without comments and transaction parts.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;example&quot;&gt;Example&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;autogenerated-migration&quot;&gt;Autogenerated migration:&lt;&#x2F;h5&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; django&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;db&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; migrations&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt; Migration&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;migrations&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    dependencies&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        (&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;team_management&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;0001_auto_20240319_1146&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    operations&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        migrations&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;RemoveField&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#001080, #9CDCFE);&quot;&gt;            model_name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;organization&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#001080, #9CDCFE);&quot;&gt;            name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;session_expiration_time&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        )&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h5 id=&quot;pr-1&quot;&gt;PR 1&lt;&#x2F;h5&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; django&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;db&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; migrations&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt; Migration&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;migrations&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    dependencies&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        (&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;team_management&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;00001_auto_20240319_1146&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    operations&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        migrations&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;SeparateDatabaseAndState&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#001080, #9CDCFE);&quot;&gt;            state_operations&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                migrations&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;RemoveField&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#001080, #9CDCFE);&quot;&gt;                    model_name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;organization&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#001080, #9CDCFE);&quot;&gt;                    name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;session_expiration_time&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                )&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        )&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h5 id=&quot;pr-2&quot;&gt;PR 2&lt;&#x2F;h5&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; django&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;db&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#AF00DB, #C586C0);&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; migrations&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#0000FF, #569CD6);&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt; Migration&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;migrations&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#267F99, #4EC9B0);&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    dependencies&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        (&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;team_management&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;0002_auto_20240328_1527&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    operations&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        migrations&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;SeparateDatabaseAndState&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#001080, #9CDCFE);&quot;&gt;            database_operations&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                migrations&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span&gt;RunSQL&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#001080, #9CDCFE);&quot;&gt;                    sql&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;                        &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;ALTER TABLE &amp;quot;team_management_organization&amp;quot; DROP COLUMN &amp;quot;session_expiration_time&amp;quot; CASCADE;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#A31515, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                    )&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                )&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        )&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Flutter tests in gitlab pipelines</title>
        <published>2024-02-25T13:23:17+10:00</published>
        <updated>2024-02-25T13:23:17+10:00</updated>
        
        <author>
          <name>
            
              Igor Shtein
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shtein.me/posts/flutter-gitlab-tests/"/>
        <id>https://shtein.me/posts/flutter-gitlab-tests/</id>
        
        <content type="html" xml:base="https://shtein.me/posts/flutter-gitlab-tests/">&lt;p&gt;Lets save you and future me 5 minutes of precious time as of how to parse the dart tests and coverage results in gitlab pipeline.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tl-dr&quot;&gt;tl;dr&lt;&#x2F;h2&gt;
&lt;p&gt;In your &lt;code&gt;.gitlab-ci.yml&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#000000, #D4D4D4); background-color: light-dark(#FFFFFF, #1E1E1E);&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;i&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;mage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; i&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;nstrumentisto&#x2F;flutter:latest&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;s&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;tages&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; t&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;est&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;t&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;est&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;  s&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;tage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; t&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;est&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;  b&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;efore_script&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; e&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;xport PATH=&amp;quot;$PATH&amp;quot;:&amp;quot;$HOME&#x2F;.pub-cache&#x2F;bin&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; f&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;lutter pub get&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; f&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;lutter clean&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; f&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;lutter pub global activate junitreport&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; f&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;lutter pub global activate cobertura&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;  s&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;cript&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; f&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;lutter test --coverage --machine | tojunit -o report.xml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; g&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;enhtml coverage&#x2F;lcov.info --output=coverage&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; c&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;obertura convert&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;  c&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;overage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;&#x2F;^\s*lines\.+: (\d+\.\d+)% .*&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;  a&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;rtifacts&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;    w&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;hen&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; a&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;lways&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;    p&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;aths&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; r&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;eport.xml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; c&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;overage&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;    r&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;eports&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;      j&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;unit&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; r&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;eport.xml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;      c&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;overage_report&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;        c&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;overage_format&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; c&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;obertura&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;        p&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;ath&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; c&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt;overage&#x2F;cobertura.xml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;    e&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#800000, #569CD6);&quot;&gt;xpire_in&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#0000FF, #CE9178);&quot;&gt; week&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;a-bit-of-explanation&quot;&gt;A bit of explanation&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;- export PATH=&quot;$PATH&quot;:&quot;$HOME&#x2F;.pub-cache&#x2F;bin&quot;&lt;&#x2F;code&gt; - Is needed to make packages we install globally callable by the &lt;strong&gt;script&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;- flutter pub get&lt;&#x2F;code&gt; - You likely need your stuff to run tests don&#x27;t you?&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;- flutter pub global activate junitreport&lt;&#x2F;code&gt; - &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pub.dev&#x2F;packages&#x2F;junitreport&quot;&gt;A package&lt;&#x2F;a&gt; that converts test results output of dart to a junit format which gitlab understands&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;- flutter pub global activate cobertura&lt;&#x2F;code&gt; - &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pub.dev&#x2F;packages&#x2F;cobertura&quot;&gt;A package&lt;&#x2F;a&gt; that converts coverage report from &lt;code&gt;lcov&lt;&#x2F;code&gt; format used by dart into &lt;code&gt;cobertura.xml&lt;&#x2F;code&gt; that gitlab understands.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;| tojunit -o report.xml&lt;&#x2F;code&gt; - tests conversion, the resulting path will be &lt;code&gt;%project_root%&#x2F;report.xml&lt;&#x2F;code&gt;, feel free to change to your liking, but you&#x27;ll also need to adjust the &lt;code&gt;artifacts&lt;&#x2F;code&gt; section of the step accordingly.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;- genhtml coverage&#x2F;lcov.info --output=coverage&lt;&#x2F;code&gt; - Conversion dart coverage result into an xml report. By default, dart test suit outputs it to the &lt;code&gt;coverage&#x2F;lcov.info&lt;&#x2F;code&gt;. This command converts &lt;code&gt;lcov.info&lt;&#x2F;code&gt; into xml parsable by the cobertura. You&#x27;ll need &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;search?keywords=lcov&quot;&gt;lcov&lt;&#x2F;a&gt; OS package to be installed for this to work, I use image &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;hub.docker.com&#x2F;r&#x2F;instrumentisto&#x2F;flutter&quot;&gt;instrumentisto&#x2F;flutter&lt;&#x2F;a&gt; with it being included.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;cobertura convert&lt;&#x2F;code&gt; - Gives us a final result. Cobertura package will generate a file called &lt;code&gt;cobertura.xml&lt;&#x2F;code&gt; in a directory you tell it, in this case its &lt;code&gt;%project_root&#x2F;coverage&#x2F;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;coverage: &#x27;&#x2F;^\s*lines\.+: (\d+\.\d+)% .*&#x2F;&#x27;&lt;&#x2F;code&gt; - Regexp to parse the coverage percentage which will be shown as your pipeline result and in you repo stats. (Se pictures below, just don&#x27;t look at the number. Really, don&#x27;t🫠)
&lt;img src=&quot;https:&#x2F;&#x2F;shtein.me&#x2F;posts&#x2F;flutter-gitlab-tests&#x2F;img&#x2F;coverage_in_job.png&quot; alt=&quot;Coverage percent in a job&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;shtein.me&#x2F;posts&#x2F;flutter-gitlab-tests&#x2F;img&#x2F;coverage_in_repo_stats.png&quot; alt=&quot;Coverage in the repo&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In the &lt;code&gt;artifacts&lt;&#x2F;code&gt; part we are telling gitlab where to find the reports and what each of them mean. Adjust the paths here if you changed them previously.&lt;&#x2F;p&gt;
&lt;p&gt;As a result you can see individual test results, the execution time in the piepline, and a coverage in the MR diff and the pipeline.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>OpenVPN server on Truenas Scale</title>
        <published>2022-08-06T00:30:17+03:00</published>
        <updated>2022-08-06T00:30:17+03:00</updated>
        
        <author>
          <name>
            
              Igor Shtein
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shtein.me/posts/truenas-openvpn/"/>
        <id>https://shtein.me/posts/truenas-openvpn/</id>
        
        <content type="html" xml:base="https://shtein.me/posts/truenas-openvpn/">&lt;h2 id=&quot;the-setup&quot;&gt;The setup&lt;&#x2F;h2&gt;
&lt;p&gt;So you have a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.truenas.com&#x2F;truenas-scale&#x2F;&quot;&gt;truenas scale&lt;&#x2F;a&gt; server, which holds all your most &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Internet_meme&quot;&gt;precious data&lt;&#x2F;a&gt;. And chances are this is a machine you want to be able to access from anywhere because of this. Maybe even you want to actually utilize the sharing capabilities of your NAS, like Samba to use your data.&lt;&#x2F;p&gt;
&lt;p&gt;Good use of resources would be to turn you NAS into a VPN server. This is done really easily on Truenas scale specifically: basically one press of a button and you are done.&lt;&#x2F;p&gt;
&lt;p&gt;So I assume you already have you OpenVpn server up and running and can connect to the Nas via it.
&lt;img src=&quot;https:&#x2F;&#x2F;shtein.me&#x2F;posts&#x2F;truenas-openvpn&#x2F;img&#x2F;setup.png&quot; alt=&quot;setup&quot; &#x2F;&gt;
In my case the 10.0.0.0\24 network is a VPN and the 192.168.1.0\24 is a remote LAN. In your case ranges can differ.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;&#x2F;h2&gt;
&lt;p&gt;But what if you now want to be able to access any device in that network. Like if it&#x27;s your home network, and you have a couple of useful devices there which you don&#x27;t want or cant connect to the VPN individually.
So our goal here is to be able to connect to 192.168.1.1 via VPN while only the 192.168.1.5 is connected to the VPN (a VPN server in our case)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-solution&quot;&gt;The solution&lt;&#x2F;h2&gt;
&lt;p&gt;In order to access the LAN of your Truenas Scale server you&#x27;ll need to enable an ip forwarding. Between the interfaces on the NAS. This will allow traffic to flow freely from the VPN network into LAN and back, giving you easy access to the resources in the LAN.
All the actions we require need to be done from advanced system settings so navigate to &lt;code&gt;System Settings&lt;&#x2F;code&gt; -&amp;gt; &lt;code&gt;Advanced&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Add a sysctl variable named &lt;code&gt;net.ipv4.ip_forward&lt;&#x2F;code&gt; with value &lt;code&gt;1&lt;&#x2F;code&gt; and enabled checkbox ticked.&lt;&#x2F;li&gt;
&lt;li&gt;On a neighboring panel called &#x27;Init&#x2F;Shutdown Scripts&#x27; add following 3 &lt;code&gt;Command&lt;&#x2F;code&gt;s with the names you will understand:
&lt;ol&gt;
&lt;li&gt;Command: &lt;code&gt;nft add table ip nat&lt;&#x2F;code&gt;, When: &lt;code&gt;Post init&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Command: &lt;code&gt;nft &#x27;add chain ip nat prerouting { type nat hook prerouting priority 0 ; }&#x27;&lt;&#x2F;code&gt;, When: &lt;code&gt;Post init&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Command: &lt;code&gt;nft &#x27;add chain ip nat postrouting { type nat hook postrouting priority 100 ; }&#x27;&lt;&#x2F;code&gt;, When: &lt;code&gt;Post init&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;We also need a 4-th &lt;code&gt;Command&lt;&#x2F;code&gt;, but you need to know 3 values in order to form it correctly. The easiest way to do it is to navigate to &lt;code&gt;System settings&lt;&#x2F;code&gt; -&amp;gt; &lt;code&gt;Shell&lt;&#x2F;code&gt; and execute the &lt;code&gt;ip address&lt;&#x2F;code&gt; command. &lt;img src=&quot;https:&#x2F;&#x2F;shtein.me&#x2F;posts&#x2F;truenas-openvpn&#x2F;img&#x2F;ifaces.png&quot; alt=&quot;ifaces&quot; &#x2F;&gt; you can find the necessary values by searching for IPs you recognize as the on from the LAN and the one from the VPN networks. The values you need are:
&lt;ul&gt;
&lt;li&gt;Name of the openvpn network interface. Mine is &lt;code&gt;openvpn-server&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Name of the lan interface. Mine is &lt;code&gt;enp2s0&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;The VPN subnet CIDR. Mine is &lt;code&gt;10.0.0.1&#x2F;24&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Add the 4-th command replacing my values with yours. The Command will be &lt;code&gt;nft &#x27;add rule nat postrouting iifname openvpn-server oifname enp2s0 ip saddr 10.0.0.1&#x2F;24 masquerade&#x27;&lt;&#x2F;code&gt;, and when the field is  &lt;code&gt;Post init&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;THe last and really optional step is to make your clients know how to route traffic to your server&#x27;s LAN. In order to do that - navigate to the &lt;code&gt;Network&lt;&#x2F;code&gt; -&amp;gt; &lt;code&gt;Open VPN&lt;&#x2F;code&gt; -&amp;gt; &lt;code&gt;Server&lt;&#x2F;code&gt; and add &lt;code&gt;push &quot;route 192.168.1.0 255.255.255.0&quot;&lt;&#x2F;code&gt; to &lt;code&gt;Addtional parameters&lt;&#x2F;code&gt; field. &lt;img src=&quot;https:&#x2F;&#x2F;shtein.me&#x2F;posts&#x2F;truenas-openvpn&#x2F;img&#x2F;push_route.png&quot; alt=&quot;push_route&quot; &#x2F;&gt; Replacing the ip with your LAN ip ofc.
That&#x27;s it 🎉. Now reboot your NAS. And go verify on the client if everything worked.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;verification&quot;&gt;Verification&lt;&#x2F;h2&gt;
&lt;p&gt;Connect to the VPN from your client and try to ping or access in any other way the ip from your server&#x27;s LAN (192.168.1.1 in my case).
&lt;img src=&quot;https:&#x2F;&#x2F;shtein.me&#x2F;posts&#x2F;truenas-openvpn&#x2F;img&#x2F;mtr.png&quot; alt=&quot;works&quot; &#x2F;&gt;
It works, yay!&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>About</title>
        <published>2022-06-22T00:00:00+00:00</published>
        <updated>2022-06-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Igor Shtein
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shtein.me/about/"/>
        <id>https://shtein.me/about/</id>
        
        <content type="html" xml:base="https://shtein.me/about/">&lt;p&gt;Welcome to my little corner of the internet 👋&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m Igor. By day I build backend services and distributed systems; the rest of
the time I tinker with all sorts of other tech such as Flutter, SDR, self-host more than is reasonable, and work
from wherever I happen to be. This site is where I write down the things I know
I&#x27;ll want to look up again later.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;i-am-open-for-work&quot;&gt;I am open for work.&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a class=&quot;resume-btn&quot; href=&quot;&#x2F;Igor Shtein CV 2026 sc.pdf&quot;&gt;View Resume 📝&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;div class=&quot;terminal&quot;&gt;
  &lt;div class=&quot;terminal-bar&quot;&gt;
    &lt;div class=&quot;terminal-title&quot;&gt;igor@shtein:~&lt;&#x2F;div&gt;
    &lt;div class=&quot;terminal-controls&quot;&gt;
      &lt;span class=&quot;min&quot;&gt;&lt;&#x2F;span&gt;
      &lt;span class=&quot;max&quot;&gt;&lt;&#x2F;span&gt;
      &lt;span class=&quot;close&quot;&gt;&lt;&#x2F;span&gt;
    &lt;&#x2F;div&gt;
  &lt;&#x2F;div&gt;
  &lt;div class=&quot;terminal-body&quot;&gt;
    &lt;p class=&quot;term-host&quot;&gt;igor@shtein:~&lt;&#x2F;p&gt;
    &lt;p class=&quot;term-cmd&quot;&gt;$ whoami&lt;&#x2F;p&gt;
    &lt;p class=&quot;term-out&quot;&gt;I build backend services and distributed systems — Python, Rust, and the infrastructure to run them in production.&lt;&#x2F;p&gt;
    &lt;p class=&quot;term-cmd&quot;&gt;$ cat availability.txt&lt;&#x2F;p&gt;
    &lt;p class=&quot;term-out&quot;&gt;Open to full-time roles remote or on-site in Israel. If you need a technical expertise in building a back-end system of any type, or a skilled developer - let&#x27;s talk.&lt;&#x2F;p&gt;
    &lt;p class=&quot;term-cmd&quot;&gt;$ mail -s &quot;Hey 👋&quot; &lt;a href=&quot;mailto:igor@shtein.me&quot;&gt;igor@shtein.me&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
    &lt;p class=&quot;term-out&quot;&gt;&lt;&#x2F;p&gt;
  &lt;&#x2F;div&gt;
&lt;&#x2F;div&gt;
</content>
        
    </entry>
</feed>
