<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Tutorial Academy]]></title><description><![CDATA[Tutorial Academy — Practical Flutter tutorials on SOLID principles, design patterns, and testing strategies to help you write cleaner, scalable, and maintainabl]]></description><link>https://tutorialacademy.info</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1698438477608/lAtTJGEXA.png</url><title>Tutorial Academy</title><link>https://tutorialacademy.info</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 11:53:36 GMT</lastBuildDate><atom:link href="https://tutorialacademy.info/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How You Use SOLID Principles in Flutter Without Even Knowing It]]></title><description><![CDATA[If you’ve built more than a couple of Flutter screens, chances are you’ve already been applying SOLID principles without even realising it. Flutter naturally pushes us toward clean, modular design: breaking big widgets into smaller ones, separating r...]]></description><link>https://tutorialacademy.info/how-you-use-solid-principles-in-flutter-without-even-knowing-it</link><guid isPermaLink="true">https://tutorialacademy.info/how-you-use-solid-principles-in-flutter-without-even-knowing-it</guid><category><![CDATA[Flutter]]></category><category><![CDATA[SOLID principles]]></category><dc:creator><![CDATA[George Zoiade]]></dc:creator><pubDate>Thu, 04 Sep 2025 13:00:32 GMT</pubDate><content:encoded><![CDATA[<p>If you’ve built more than a couple of Flutter screens, chances are you’ve already been applying SOLID principles without even realising it. Flutter naturally pushes us toward clean, modular design: breaking big widgets into smaller ones, separating responsibilities, and reusing components across the app.</p>
<p>But here’s the thing — those habits aren’t just “good practices.” They’re actually the foundation of <strong>SOLID</strong>, a set of five design principles created to help developers write maintainable, scalable software. Even if you’ve never studied them formally, you’re most likely already applying them in your day-to-day Flutter work. Every time you create a small widget that handles one job well, or design a class that can be extended without rewriting its core logic, you’re practicing SOLID.</p>
<p>Before we dive into the Flutter examples, let’s quickly step back. <strong>SOLID</strong> is an acronym introduced by Robert C. Martin (Uncle Bob), and it stands for:</p>
<ul>
<li><p><strong>S</strong>ingle Responsibility</p>
</li>
<li><p><strong>O</strong>pen/Closed</p>
</li>
<li><p><strong>L</strong>iskov Substitution</p>
</li>
<li><p><strong>I</strong>nterface Segregation</p>
</li>
<li><p><strong>D</strong>ependency Inversion</p>
</li>
</ul>
<p>On their own, each principle is straightforward. Together, they form a powerful mindset for designing code that’s easier to test, extend, and maintain.</p>
<p>In this post, I want to take a closer look at each of the five principles and show how they quietly shape the way we build Flutter apps. I’m not going into heavy theory here; instead, I’ll use simple examples that most of us have probably written before. My goal is to show that SOLID isn’t some abstract concept you only read about in textbooks — it’s something you’re already using, and once you recognise it, you can apply it more intentionally to make your code even stronger.</p>
<hr />
<h2 id="heading-single-responsibility-principle-srp"><strong>Single Responsibility Principle (SRP)</strong></h2>
<p>The Single Responsibility Principle says that a class or function should have only one reason to change. In simpler words: <strong>do one thing, and do it well.</strong></p>
<p>In Flutter, this often happens naturally. When we start out, it’s tempting to throw everything into one big widget — UI, state, layout, and logic all living in a single file. But as soon as that widget grows too complex, we split it into smaller parts: one widget for displaying an image, another for showing a title, and maybe a separate widget for a list item. Each of these smaller widgets now has a single responsibility, which makes them easier to test, reuse, and maintain.</p>
<h3 id="heading-problem">Problem</h3>
<p>When we’re moving fast, it’s common to throw everything into a single widget. It works fine at first, but as that widget grows, it becomes harder to read, harder to test, and more painful to change later. Take this simple example of a ProductTile widget:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductTile</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ProductTile({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.imageUrl,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.price,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> imageUrl;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> price;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Row(
      children: [
        Image.network(
          imageUrl,
          width: <span class="hljs-number">50</span>,
          height: <span class="hljs-number">50</span>,
        ),
        <span class="hljs-keyword">const</span> SizedBox(
          width: <span class="hljs-number">8</span>,
        ),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            Text(
              <span class="hljs-string">'\$<span class="hljs-subst">${price.toStringAsFixed(<span class="hljs-number">2</span>)}</span>'</span>,
            ),
          ],
        ),
      ],
    );
  }
}
</code></pre>
<h3 id="heading-applying-srp-by-splitting-responsibilities"><strong>Applying SRP by Splitting Responsibilities</strong></h3>
<p>To follow the Single Responsibility Principle, we can refactor the widget into smaller building blocks. Each widget now takes care of only one concern:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductImage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ProductImage({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.imageUrl,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> imageUrl;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Image.network(
      imageUrl,
      width: <span class="hljs-number">50</span>,
      height: <span class="hljs-number">50</span>,
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductInfo</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ProductInfo({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.price,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> price;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: &lt;Widget&gt;[
        Text(
          title,
          style: Theme.of(context).textTheme.titleMedium,
        ),
        Text(
          <span class="hljs-string">'\$<span class="hljs-subst">${price.toStringAsFixed(<span class="hljs-number">2</span>)}</span>'</span>,
        ),
      ],
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductTile</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ProductTile({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.imageUrl,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.price,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> imageUrl;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> price;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Row(
      children: &lt;Widget&gt;[
        ProductImage(
          imageUrl: imageUrl,
        ),
        <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">8</span>),
        ProductInfo(
          title: title,
          price: price,
        ),
      ],
    );
  }
}
</code></pre>
<p>Now we’ve split the responsibilities:</p>
<ul>
<li><p>ProductImage → handles only how the product image is displayed</p>
</li>
<li><p>ProductInfo → handles only how the title and price are shown</p>
</li>
<li><p>ProductTile → simply coordinates the layout</p>
</li>
</ul>
<h3 id="heading-why-this-matters"><strong>Why this matters?</strong></h3>
<p>The functionality hasn’t changed, but the design is far more flexible. If we ever want to change how prices are displayed (e.g., add a discount or localise the currency), we only touch ProductInfo. If we decide to switch from Image.network to a cached image widget, we update only ProductImage.</p>
<p>That’s the power of the <strong>Single Responsibility Principle</strong>: by giving each class or widget one clear job, our code becomes easier to understand, easier to test, and easier to extend. And while this is just a small UI example, the same idea applies everywhere — from services and repositories to complex business logic.</p>
<hr />
<h2 id="heading-openclosed-principle-ocp"><strong>Open/Closed Principle (OCP)</strong></h2>
<p>The Open/Closed Principle says: <strong>classes should be open for extension, but closed for modification.</strong></p>
<p>In Flutter, this often shows up when you want to create a reusable widget. You don’t want to keep editing the widget every time you need a small variation — instead, you design it so it can be <strong>extended or configured</strong> without touching the original code.</p>
<h3 id="heading-problem-1"><strong>Problem</strong></h3>
<p>It’s tempting to add flags and conditions inside a widget to cover all cases. This works at first, but over time the widget grows harder to maintain. Every new variant means editing the same class again.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> CustomButton({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.label,
    <span class="hljs-keyword">this</span>.isPrimary = <span class="hljs-keyword">false</span>,
    <span class="hljs-keyword">this</span>.isDanger = <span class="hljs-keyword">false</span>,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> label;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> isPrimary;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> isDanger;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    Color background;
    <span class="hljs-keyword">if</span> (isPrimary) {
      background = Colors.blue;
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (isDanger) {
      background = Colors.red;
    } <span class="hljs-keyword">else</span> {
      background = Colors.grey;
    }

    <span class="hljs-keyword">return</span> ElevatedButton(
      style: ElevatedButton.styleFrom(
        backgroundColor: background,
      ),
      onPressed: () {},
      child: Text(label),
    );
  }
}
</code></pre>
<p>This works, but every new button type (success, warning, outline, etc.) forces us to <strong>modify</strong> the class by adding more flags and conditions.</p>
<p>Instead, we can design the widget so it’s <strong>configurable</strong> through parameters or by subclassing/extending styles. The base widget stays the same.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> CustomButton({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.label,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.background,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.onPressed,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> label;
  <span class="hljs-keyword">final</span> Color background;
  <span class="hljs-keyword">final</span> VoidCallback onPressed;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> ElevatedButton(
      style: ElevatedButton.styleFrom(
        backgroundColor: background,
      ),
      onPressed: onPressed,
      child: Text(label),
    );
  }
}
</code></pre>
<p>Now, you extend behaviour outside the widget:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Reuse without modifying the widget</span>
<span class="hljs-keyword">final</span> primaryButton = CustomButton(
  label: <span class="hljs-string">"Continue"</span>,
  background: Colors.blue,
  onPressed: () {},
);

<span class="hljs-keyword">final</span> dangerButton = CustomButton(
  label: <span class="hljs-string">"Delete"</span>,
  background: Colors.red,
  onPressed: () {},
);

<span class="hljs-keyword">final</span> successButton = CustomButton(
  label: <span class="hljs-string">"Saved"</span>,
  background: Colors.green,
  onPressed: () {},
);
</code></pre>
<h3 id="heading-why-this-matters-1"><strong>Why this matters?</strong></h3>
<p>The CustomButton widget no longer needs to be modified when requirements change. It’s <strong>closed for modification</strong> but <strong>open for extension</strong> — new button styles or behaviours can be added outside of it. This keeps the base widget simple and makes your UI code more scalable as your app grows.</p>
<hr />
<h2 id="heading-liskov-substitution-principle-lsp"><strong>Liskov Substitution Principle (LSP)</strong></h2>
<p>The Liskov Substitution Principle says: <strong>if you have a base class, you should be able to replace it with any of its subclasses without breaking the app.</strong></p>
<p>In Flutter, this usually shows up when we design base widgets or abstract classes. If one subclass doesn’t fully behave like the parent, it can lead to unexpected bugs.</p>
<h3 id="heading-problem-2"><strong>Problem</strong></h3>
<p>Subclasses sometimes introduce hidden restrictions or change the expected behaviour of the base class. This breaks substitutability and causes surprising runtime errors.</p>
<p>Suppose we create a base widget for displaying messages:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MessageWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> MessageWidget({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.text,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> text;
}
</code></pre>
<p>Now we make this implementation:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InfoMessage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">MessageWidget</span> </span>{
  <span class="hljs-keyword">const</span> InfoMessage({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">super</span>.text,
  });

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Text(
      text,
      style: <span class="hljs-keyword">const</span> TextStyle(
        color: Colors.blue,
      ),
    );
  }
}
</code></pre>
<p>This respects the contract: <em>any</em> string you pass will be displayed.</p>
<p>Now here’s a problematic subclass:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ShortMessage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">MessageWidget</span> </span>{
  <span class="hljs-keyword">const</span> ShortMessage({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">super</span>.text,
  }) : <span class="hljs-keyword">assert</span>(text.length &lt;= <span class="hljs-number">20</span>, <span class="hljs-string">'Text too long for ShortMessage'</span>);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Text(
      text,
      style: <span class="hljs-keyword">const</span> TextStyle(
        color: Colors.orange,
      ),
    );
  }
}
</code></pre>
<p>At first glance this seems fine, but notice the hidden rule: the base class allowed <em>any</em> text, while this subclass suddenly refuses longer strings.</p>
<p>That’s an <strong>LSP violation</strong>. Code that worked with InfoMessage will now fail with ShortMessage.</p>
<h3 id="heading-correct-approach"><strong>Correct approach</strong></h3>
<p>If you need a different presentation, do it without changing the contract:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WarningMessage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">MessageWidget</span> </span>{
  <span class="hljs-keyword">const</span> WarningMessage({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">super</span>.text,
  });

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Row(
      children: &lt;Widget&gt;[
        <span class="hljs-keyword">const</span> Icon(Icons.warning_amber_rounded),
        <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">6</span>),
        Flexible(
          child: Text(
            text,
            style: <span class="hljs-keyword">const</span> TextStyle(
              color: Colors.orange,
              fontWeight: FontWeight.bold,
            ),
            overflow: TextOverflow.ellipsis,
          ),
        ),
      ],
    );
  }
}
</code></pre>
<p>This widget still accepts any string (no hidden assertions), but adds extra visuals (icon, style, ellipsis). It <strong>extends behaviour</strong> without breaking the contract.</p>
<h3 id="heading-why-this-matters-2"><strong>Why this matters?</strong></h3>
<p>With LSP, consistency is the key: subclasses must not add extra restrictions or silently change what the base promised. In Flutter, that often means avoiding hidden asserts or silent logic changes in subclasses. Stick to the contract, and your widgets will remain interchangeable and predictable.</p>
<hr />
<h2 id="heading-interface-segregation-principle-isp"><strong>Interface Segregation Principle (ISP)</strong></h2>
<p>The Interface Segregation Principle says: <strong>don’t force classes to depend on methods they don’t use.</strong></p>
<p>In other words: instead of one big “god” interface with too many responsibilities, break it into smaller, focused contracts. That way, each implementation only cares about what it actually needs.</p>
<h3 id="heading-problem-3"><strong>Problem</strong></h3>
<p>When an interface defines too many responsibilities, some classes end up implementing methods they don’t need. This leads to unnecessary boilerplate and code that feels “forced.”</p>
<pre><code class="lang-dart"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoadableWidget</span> </span>{
  <span class="hljs-keyword">void</span> loadData();
  <span class="hljs-keyword">void</span> refresh();
  <span class="hljs-keyword">void</span> cancel();
  <span class="hljs-keyword">void</span> saveToCache();
}
</code></pre>
<p>Now, if we implement this in a simple widget like an <strong>Image widget</strong>, we’re stuck:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImageLoaderWidget</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">LoadableWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> loadData() {
    <span class="hljs-comment">// ✅ loads an image</span>
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> refresh() {
    <span class="hljs-comment">// ✅ maybe reload the image</span>
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> cancel() {
    <span class="hljs-comment">// ✅ cancel a network request</span>
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> saveToCache() {
    <span class="hljs-comment">// ❌ but what if this widget doesn’t care about caching?</span>
    <span class="hljs-keyword">throw</span> UnimplementedError();
  }
}
</code></pre>
<p>This breaks ISP — the widget is <strong>forced</strong> to implement methods (saveToCache) it doesn’t actually need.</p>
<h3 id="heading-smaller-focused-interfaces"><strong>Smaller, focused interfaces</strong></h3>
<p>Instead, we break the big interface into smaller, <strong>segregated</strong> ones:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Loadable</span> </span>{
  <span class="hljs-keyword">void</span> loadData();
}

<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Refreshable</span> </span>{
  <span class="hljs-keyword">void</span> refresh();
}

<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Cancellable</span> </span>{
  <span class="hljs-keyword">void</span> cancel();
}

<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Cacheable</span> </span>{
  <span class="hljs-keyword">void</span> saveToCache();
}
</code></pre>
<p>Now each widget can pick and choose:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImageLoaderWidget</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Loadable</span>, <span class="hljs-title">Refreshable</span>, <span class="hljs-title">Cancellable</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> loadData() {
    <span class="hljs-comment">// load image</span>
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> refresh() {
    <span class="hljs-comment">// reload image</span>
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> cancel() {
    <span class="hljs-comment">// cancel image request</span>
  }
}
</code></pre>
<p>And if we later add a widget that actually cares about caching (like a data-heavy API widget), it can also implement Cacheable without affecting the others.</p>
<h3 id="heading-why-this-matters-3"><strong>Why this matters?</strong></h3>
<p>With ISP, your code stays <strong>flexible and clean</strong>:</p>
<ul>
<li><p>Widgets (or services) only depend on what they actually use.</p>
</li>
<li><p>No more dummy methods or throw UnimplementedError.</p>
</li>
<li><p>Easier to test, because each contract is small and focused.</p>
</li>
</ul>
<p>In Flutter, this principle often appears in <strong>repositories, services, or widget contracts</strong>. By splitting them into smaller interfaces, you avoid coupling features that don’t belong together.</p>
<hr />
<h2 id="heading-dependency-inversion-principle-dip"><strong>Dependency Inversion Principle (DIP)</strong></h2>
<p>The Dependency Inversion Principle says: <strong>high-level modules shouldn’t depend on low-level modules; both should depend on abstractions.</strong></p>
<p>In Flutter, this can apply to widgets too. If a widget directly depends on a specific concrete class, it becomes rigid. But if it depends on an abstraction (like a callback, a builder, or an interface), you can swap implementations without rewriting the widget.</p>
<h3 id="heading-problem-4"><strong>Problem</strong></h3>
<p>When a widget creates or uses concrete classes directly, it gets tightly coupled to that implementation. This makes the widget harder to test and less reusable.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ProfileWidget({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-comment">// ❌ tightly coupled: ProfileWidget directly depends on ApiService</span>
    <span class="hljs-keyword">final</span> api = ApiService();
    <span class="hljs-keyword">final</span> user = api.getUser();

    <span class="hljs-keyword">return</span> Text(user.name);
  }
}
</code></pre>
<p>This widget can’t be tested easily and is locked to ApiService.</p>
<h4 id="heading-a-widget-depending-on-an-abstraction"><strong>A widget depending on an abstraction</strong></h4>
<p>Instead, we invert the dependency: the widget just expects <strong>something that can provide a user</strong>. That “something” could be an interface, a function, or a provider.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserProvider</span> </span>{
  <span class="hljs-built_in">String</span> getUserName();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiUserProvider</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">UserProvider</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">String</span> getUserName() =&gt; <span class="hljs-string">'User from API'</span>;
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MockUserProvider</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">UserProvider</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">String</span> getUserName() =&gt; <span class="hljs-string">'Test User'</span>;
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ProfileWidget({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.userProvider,
  });

  <span class="hljs-keyword">final</span> UserProvider userProvider;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Text(userProvider.getUserName());
  }
}
</code></pre>
<p>And this is how you would use it:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// In production</span>
ProfileWidget(ApiUserProvider());

<span class="hljs-comment">// In tests</span>
ProfileWidget(MockUserProvider());
</code></pre>
<p>Now the widget <strong>doesn’t care</strong> where the user data comes from — it depends on the abstraction UserProvider, not on the concrete ApiService.</p>
<h3 id="heading-even-simpler-callback-based-dip"><strong>Even simpler: callback-based DIP</strong></h3>
<p>Sometimes you don’t even need an interface — a function parameter works too:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ProfileWidget({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.getUserName,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> <span class="hljs-built_in">Function</span>() getUserName;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Text(getUserName());
  }
}
</code></pre>
<p>Now you can inject any behaviour from the outside:</p>
<pre><code class="lang-dart">ProfileWidget(getUserName: () =&gt; <span class="hljs-string">'User from API'</span>);
ProfileWidget(getUserName: () =&gt; <span class="hljs-string">'Test User'</span>);
</code></pre>
<h3 id="heading-why-this-matters-4"><strong>Why this matters?</strong></h3>
<p>By applying DIP to widgets:</p>
<ul>
<li><p>You make them <strong>reusable</strong> (UI doesn’t lock into a specific data source).</p>
</li>
<li><p>You make them <strong>testable</strong> (inject mocks or fakes easily).</p>
</li>
<li><p>You follow the same principle used in services and repositories, but at the <strong>UI level</strong>.</p>
</li>
</ul>
<hr />
<h2 id="heading-wrapping-up-solid-in-flutter"><strong>Wrapping Up: SOLID in Flutter</strong></h2>
<p>The SOLID principles might sound abstract when you first read about them, but in practice, they’re already part of how we build Flutter apps every day.</p>
<ul>
<li><p><strong>Single Responsibility (SRP)</strong> → Avoid “god widgets.” Break them down so each one has a single purpose. ✅ Clearer, smaller, easier to test.</p>
</li>
<li><p><strong>Open/Closed (OCP)</strong> → Don’t keep editing widgets for every new variant. Design them to be extended instead. ✅ More reusable, less fragile.</p>
</li>
<li><p><strong>Liskov Substitution (LSP)</strong> → Subclasses should behave consistently with their base class. ✅ Predictable, no hidden surprises.</p>
</li>
<li><p><strong>Interface Segregation (ISP)</strong> → Don’t force widgets (or services) to implement methods they don’t need. ✅ Cleaner contracts, simpler tests.</p>
</li>
<li><p><strong>Dependency Inversion (DIP)</strong> → Depend on abstractions, not concrete classes. ✅ More flexible, easier to mock, future-proof.</p>
</li>
</ul>
<p>Individually, each principle improves code clarity. Together, they form a mindset: <strong>design your widgets and classes so they’re easy to understand, easy to extend, and hard to break.</strong></p>
<p>And that’s the real value of SOLID — it’s not just theory, it’s a practical way to keep your Flutter codebase scalable as your app (and your team) grows.</p>
<hr />
<h3 id="heading-which-of-these-principles-do-you-realise-youve-already-been-applying-in-your-flutter-code-without-even-knowing"><strong>Which of these principles do you realise you’ve already been applying in your Flutter code without even knowing?</strong> 🤔</h3>
]]></content:encoded></item></channel></rss>