<?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[Temi Ajiboye's Blog]]></title><description><![CDATA[I am a senior Flutter Developer with extensive experience in building scalable web applications and windows services.

My experiences range from building highly]]></description><link>https://article.temiajiboye.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1724253339057/eb8e5670-5c25-452f-ac06-87691cabf5ae.png</url><title>Temi Ajiboye&apos;s Blog</title><link>https://article.temiajiboye.com</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 20 May 2026 04:16:51 GMT</lastBuildDate><atom:link href="https://article.temiajiboye.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Simplifying State Management in Flutter Using ValueNotifier and ValueListenableBuilder]]></title><description><![CDATA[State management is a crucial aspect of building Flutter applications.
In this article, we'll explore how to use ValueNotifier and ValueListenableBuilder for effective state management, from simple use cases to complex scenarios.
We'll build an e-com...]]></description><link>https://article.temiajiboye.com/simplifying-state-management-in-flutter-using-valuenotifier-and-valuelistenablebuilder</link><guid isPermaLink="true">https://article.temiajiboye.com/simplifying-state-management-in-flutter-using-valuenotifier-and-valuelistenablebuilder</guid><category><![CDATA[2Articles1Week]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Examples]]></category><category><![CDATA[Flutter Widgets]]></category><category><![CDATA[flutter]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter SDK]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[Dart]]></category><category><![CDATA[#dart-for-beginners]]></category><category><![CDATA[dart programming tutorial]]></category><dc:creator><![CDATA[Temitope Ajiboye]]></dc:creator><pubDate>Wed, 11 Sep 2024 17:32:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726075839983/17cd17c3-0b24-4844-9ff3-c8d4b560a36a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>State management is a crucial aspect of building Flutter applications.</p>
<p>In this article, we'll explore how to use ValueNotifier and ValueListenableBuilder for effective state management, from simple use cases to complex scenarios.</p>
<p>We'll build an e-commerce app to demonstrate these concepts in action.</p>
<h2 id="heading-basic-concepts">Basic Concepts</h2>
<h3 id="heading-valuenotifier">ValueNotifier</h3>
<p>ValueNotifier is a simple way to wrap a value and notify listeners when that value changes. It's part of Flutter's foundation library.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> counter = ValueNotifier&lt;<span class="hljs-built_in">int</span>&gt;(<span class="hljs-number">0</span>);
</code></pre>
<p>In this example, we create a <code>ValueNotifier</code> that holds an integer value, initially set to 0.</p>
<h3 id="heading-valuelistenablebuilder">ValueListenableBuilder</h3>
<p>ValueListenableBuilder is a widget that rebuilds its child when the value in a ValueNotifier changes.</p>
<pre><code class="lang-dart">ValueListenableBuilder&lt;<span class="hljs-built_in">int</span>&gt;(
  valueListenable: counter,
  builder: (context, value, child) {
    <span class="hljs-keyword">return</span> Text(<span class="hljs-string">'Count: <span class="hljs-subst">$value</span>'</span>);
  },
)
</code></pre>
<p>You might ask, what is the relationship between ValueNotifier and ValueListenableBuilder?</p>
<p><code>ValueNotifier</code> is a way to wrap a value that can change over time. It's part of the data/state management side of your application.<br />In the example above, the <code>counter</code> is a ValueNotifier that holds an integer value. You can change this value like this:</p>
<pre><code class="lang-dart">counter.value = <span class="hljs-number">1</span>;
</code></pre>
<p>However, just changing the value doesn't automatically update your UI. This is where ValueListenableBuilder comes in. It's a widget that listens to a ValueNotifier and rebuilds its child whenever the notifier's value changes. In the example above, the builder listens to the counter notifier to rebuild it’s child when the value changes.</p>
<p>The ValueListenableBuilder is the bridge between your data (ValueNotifier) and your UI. It's responsible for:</p>
<ul>
<li><p>Listening to changes in the ValueNotifier</p>
</li>
<li><p>Rebuilding the widget tree when the value changes</p>
</li>
<li><p>Providing the current value to its builder function</p>
</li>
</ul>
<p>Without ValueListenableBuilder:</p>
<ul>
<li><p>Your UI wouldn't know when to update in response to changes in the ValueNotifier.</p>
</li>
<li><p>You'd have to manually trigger rebuilds of your widget tree every time the value changes, which would be inefficient and error-prone.</p>
</li>
</ul>
<p>Combining them together:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CounterPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">final</span> counter = ValueNotifier&lt;<span class="hljs-built_in">int</span>&gt;(<span class="hljs-number">0</span>);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      body: Center(
        child: ValueListenableBuilder&lt;<span class="hljs-built_in">int</span>&gt;(
          valueListenable: counter,
          builder: (context, value, child) {
            <span class="hljs-keyword">return</span> Text(<span class="hljs-string">'Count: <span class="hljs-subst">$value</span>'</span>);
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () =&gt; counter.value++,
        child: Icon(Icons.add),
      ),
    );
  }
}
</code></pre>
<p>In this example:</p>
<ul>
<li><p>The ValueNotifier (<code>counter</code>) holds the state.</p>
</li>
<li><p>The ValueListenableBuilder listens for changes to <code>counter</code> and updates the Text widget.</p>
</li>
<li><p>When the FloatingActionButton is pressed, it increments <code>counter.value</code>.</p>
</li>
<li><p>The ValueListenableBuilder detects this change and rebuilds the Text widget with the new value.</p>
</li>
</ul>
<p>Without the ValueListenableBuilder, the Text widget wouldn't update when <code>counter.value</code> changes.</p>
<p>Now that we have got the basics out of the way, let’s deep dive some advanced concepts.</p>
<h2 id="heading-multivaluelistenablebuilder">MultiValueListenableBuilder</h2>
<p>When dealing with multiple <code>ValueNotifier</code>s, we can create a custom <code>MultiValueListenableBuilder</code> to simplify our code:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MultiValueListenableBuilder</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;ValueListenable&gt; valueListenables;
  <span class="hljs-keyword">final</span> Widget <span class="hljs-built_in">Function</span>(BuildContext context, <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt; values, Widget? child) builder;
  <span class="hljs-keyword">final</span> Widget? child;

  <span class="hljs-keyword">const</span> MultiValueListenableBuilder({
    Key? key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.valueListenables,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.builder,
    <span class="hljs-keyword">this</span>.child,
  }) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> ValueListenableBuilder&lt;<span class="hljs-built_in">dynamic</span>&gt;(
      valueListenable: valueListenables[<span class="hljs-number">0</span>],
      builder: (context, value, _) {
        <span class="hljs-keyword">return</span> _buildNested(context, [value], <span class="hljs-number">1</span>);
      },
    );
  }

  Widget _buildNested(BuildContext context, <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt; values, <span class="hljs-built_in">int</span> index) {
    <span class="hljs-keyword">if</span> (index &gt;= valueListenables.length) {
      <span class="hljs-keyword">return</span> builder(context, values, child);
    }
    <span class="hljs-keyword">return</span> ValueListenableBuilder&lt;<span class="hljs-built_in">dynamic</span>&gt;(
      valueListenable: valueListenables[index],
      builder: (context, value, _) {
        <span class="hljs-keyword">return</span> _buildNested(context, [...values, value], index + <span class="hljs-number">1</span>);
      },
    );
  }
}
</code></pre>
<p>Let's break down this <code>MultiValueListenableBuilder</code> class:</p>
<p>This class is designed to listen to multiple ValueNotifiers at once and rebuild a widget when any of them change.</p>
<p>It's a StatelessWidget, meaning it doesn't maintain its own state. It takes a list of ValueListenables, a builder function, and an optional child widget. It starts with the first <code>ValueListenable</code>. It then creates nested <code>ValueListenableBuilders</code> for each subsequent <code>ValueListenable</code>. At the innermost level, when all values are collected, it calls the provided builder function. If any <code>ValueListenable</code> changes, it triggers a rebuild of its corresponding <code>ValueListenableBuilder</code> and all inner ones. This creates a widget that rebuilds whenever any of the provided <code>ValueListenables</code> change. The builder function receives a list of all current values, allowing you to build your UI based on all of them.</p>
<p>In essence, this class is a more flexible version of the standard ValueListenableBuilder, allowing you to react to changes in multiple values at once, which is often needed in more complex state management scenarios.</p>
<h2 id="heading-listenablemerge">Listenable.merge</h2>
<p>An alternative to having a custom MultiValueListenableBuilder is to use the <code>Listenable.merge</code> constructor: <a target="_blank" href="https://api.flutter.dev/flutter/foundation/Listenable/Listenable.merge.html">https://api.flutter.dev/flutter/foundation/Listenable/Listenable.merge.html</a><br />With this, it returns a Listenable that triggers when any of the given <a target="_blank" href="https://api.flutter.dev/flutter/foundation/Listenable-class.html">Listenable</a>s themselves trigger.</p>
<h2 id="heading-e-commerce-app-implementation">E-Commerce App Implementation</h2>
<p>Let's build an e-commerce app using ValueNotifier and ValueListenableBuilder. We'll implement product listing, a shopping cart, and checkout functionality.</p>
<h3 id="heading-1-models">1. Models</h3>
<p>First, let's define our data models:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Product</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> name;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> price;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> imageUrl;

  Product({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.id, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.name, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.price, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.imageUrl});
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CartItem</span> </span>{
  <span class="hljs-keyword">final</span> Product product;
  <span class="hljs-keyword">final</span> ValueNotifier&lt;<span class="hljs-built_in">int</span>&gt; quantity;

  CartItem({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.product, <span class="hljs-built_in">int</span> initialQuantity = <span class="hljs-number">1</span>})
      : quantity = ValueNotifier(initialQuantity);
}
</code></pre>
<p>The <code>Product</code> class represents a product in our catalog. The <code>CartItem</code> class represents a product in the shopping cart, with a <code>ValueNotifier</code> for the quantity to allow for easy updates and UI rebuilds.</p>
<h3 id="heading-2-services">2. Services</h3>
<p>Let's simulate a backend service:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiService</span> </span>{
  Future&lt;<span class="hljs-built_in">List</span>&lt;Product&gt;&gt; fetchProducts() <span class="hljs-keyword">async</span> {
    <span class="hljs-comment">// Simulate API delay</span>
    <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>));
    <span class="hljs-keyword">return</span> [
      Product(id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Laptop'</span>, price: <span class="hljs-number">999.99</span>, imageUrl: <span class="hljs-string">'https://example.com/laptop.jpg'</span>),
      Product(id: <span class="hljs-number">2</span>, name: <span class="hljs-string">'Smartphone'</span>, price: <span class="hljs-number">499.99</span>, imageUrl: <span class="hljs-string">'https://example.com/smartphone.jpg'</span>),
      Product(id: <span class="hljs-number">3</span>, name: <span class="hljs-string">'Headphones'</span>, price: <span class="hljs-number">99.99</span>, imageUrl: <span class="hljs-string">'https://example.com/headphones.jpg'</span>),
    ];
  }

  Future&lt;<span class="hljs-built_in">bool</span>&gt; submitOrder(<span class="hljs-built_in">List</span>&lt;CartItem&gt; items, <span class="hljs-built_in">double</span> total) <span class="hljs-keyword">async</span> {
    <span class="hljs-comment">// Simulate API delay and always return success for this example</span>
    <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>));
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  }
}
</code></pre>
<p>This <code>ApiService</code> class simulates API calls to fetch products and submit orders.</p>
<h3 id="heading-3-state-management">3. State Management</h3>
<p>Now, let's implement our state management classes:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductCatalogNotifier</span> </span>{
  <span class="hljs-keyword">final</span> ApiService _apiService;
  <span class="hljs-keyword">final</span> ValueNotifier&lt;<span class="hljs-built_in">List</span>&lt;Product&gt;&gt; _products = ValueNotifier([]);
  <span class="hljs-keyword">final</span> ValueNotifier&lt;<span class="hljs-built_in">bool</span>&gt; _isLoading = ValueNotifier(<span class="hljs-keyword">false</span>);
  <span class="hljs-keyword">final</span> ValueNotifier&lt;<span class="hljs-built_in">String?</span>&gt; _error = ValueNotifier(<span class="hljs-keyword">null</span>);

  ProductCatalogNotifier(<span class="hljs-keyword">this</span>._apiService);

  ValueNotifier&lt;<span class="hljs-built_in">List</span>&lt;Product&gt;&gt; <span class="hljs-keyword">get</span> products =&gt; _products;
  ValueNotifier&lt;<span class="hljs-built_in">bool</span>&gt; <span class="hljs-keyword">get</span> isLoading =&gt; _isLoading;
  ValueNotifier&lt;<span class="hljs-built_in">String?</span>&gt; <span class="hljs-keyword">get</span> error =&gt; _error;

  Future&lt;<span class="hljs-keyword">void</span>&gt; fetchProducts() <span class="hljs-keyword">async</span> {
    _isLoading.value = <span class="hljs-keyword">true</span>;
    _error.value = <span class="hljs-keyword">null</span>;
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> fetchedProducts = <span class="hljs-keyword">await</span> _apiService.fetchProducts();
      _products.value = fetchedProducts;
    } <span class="hljs-keyword">catch</span> (e) {
      _error.value = <span class="hljs-string">'Failed to fetch products: <span class="hljs-subst">${e.toString()}</span>'</span>;
    } <span class="hljs-keyword">finally</span> {
      _isLoading.value = <span class="hljs-keyword">false</span>;
    }
  }

  <span class="hljs-keyword">void</span> dispose() {
    _products.dispose();
    _isLoading.dispose();
    _error.dispose();
  }
}
</code></pre>
<p>This class encapsulates all the logic for managing product catalog state. By using <code>ValueNotifiers</code>, it can notify listeners (like UI widgets) whenever the products, loading state, or error state changes, allowing for reactive updates to the user interface.</p>
<p>The <code>ProductCatalogNotifier</code> manages the state of our product catalog. It uses separate <code>ValueNotifier</code>s for the product list, loading state, and error state. The fetchProducts method updates these states as it fetches products from the API.</p>
<p>Now, we need a cart to store our products before we submit them.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CartNotifier</span> </span>{
  <span class="hljs-keyword">final</span> ValueNotifier&lt;<span class="hljs-built_in">List</span>&lt;CartItem&gt;&gt; _items = ValueNotifier([]);
  <span class="hljs-keyword">final</span> ValueNotifier&lt;<span class="hljs-built_in">double</span>&gt; _totalPrice = ValueNotifier(<span class="hljs-number">0.0</span>);

  ValueNotifier&lt;<span class="hljs-built_in">List</span>&lt;CartItem&gt;&gt; <span class="hljs-keyword">get</span> items =&gt; _items;
  ValueNotifier&lt;<span class="hljs-built_in">double</span>&gt; <span class="hljs-keyword">get</span> totalPrice =&gt; _totalPrice;

  <span class="hljs-keyword">void</span> addItem(Product product) {
    <span class="hljs-keyword">final</span> existingItem = _items.value.firstWhere(
      (item) =&gt; item.product.id == product.id,
      orElse: () =&gt; CartItem(product: product, initialQuantity: <span class="hljs-number">0</span>),
    );

    <span class="hljs-keyword">if</span> (existingItem.quantity.value == <span class="hljs-number">0</span>) {
      _items.value = [..._items.value, existingItem];
    }
    existingItem.quantity.value++;
    _updateTotalPrice();
  }

  <span class="hljs-keyword">void</span> removeItem(Product product) {
    _items.value = _items.value.where((item) =&gt; item.product.id != product.id).toList();
    _updateTotalPrice();
  }

  <span class="hljs-keyword">void</span> _updateTotalPrice() {
    _totalPrice.value = _items.value.fold(
      <span class="hljs-number">0.0</span>,
      (total, item) =&gt; total + (item.product.price * item.quantity.value),
    );
  }

  <span class="hljs-keyword">void</span> dispose() {
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> _items.value) {
      item.quantity.dispose();
    }
    _items.dispose();
    _totalPrice.dispose();
  }
}
</code></pre>
<ol>
<li><p>Purpose:</p>
<p> This class manages the state of a shopping cart, including the items in the cart and the total price.</p>
</li>
<li><p>Properties:</p>
<p> <code>_items</code>: A ValueNotifier holding a list of CartItem objects. It represents the items in the cart.</p>
<p> <code>_totalPrice</code>: A ValueNotifier holding a double. It represents the total price of all items in the cart.</p>
</li>
<li><p>Getters:</p>
<p> Provide access to the ValueNotifiers, allowing other parts of the app to listen to changes in the cart items and total price.</p>
</li>
<li><p><code>addItem</code> method:</p>
<ul>
<li><p>Checks if the product already exists in the cart.</p>
</li>
<li><p>If it doesn't exist or has quantity 0, adds it to the cart.</p>
</li>
<li><p>Increments the quantity of the item.</p>
</li>
<li><p>Updates the total price.</p>
</li>
</ul>
</li>
<li><p><code>removeItem</code> method:</p>
<ul>
<li><p>Removes an item from the cart based on the product ID.</p>
</li>
<li><p>Updates the total price.</p>
</li>
</ul>
</li>
<li><p><code>_updateTotalPrice</code> method:</p>
<ul>
<li><p>Calculates the total price of all items in the cart.</p>
</li>
<li><p>Uses the fold method to sum up the prices of all items, considering their quantities.</p>
</li>
</ul>
</li>
<li><p><code>dispose</code> method:</p>
<ul>
<li>Cleans up resources by disposing of all ValueNotifiers, including those in individual CartItems.</li>
</ul>
</li>
</ol>
<p>This class encapsulates all the logic for managing the shopping cart state. By using ValueNotifiers, it can notify listeners (like UI widgets) whenever the cart items or total price changes, allowing for reactive updates to the user interface.</p>
<h3 id="heading-4-ui-implementation">4. UI Implementation</h3>
<p>Now, let's implement the UI for our product list page:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductListPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _ProductListPageState createState() =&gt; _ProductListPageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_ProductListPageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">ProductListPage</span>&gt; </span>{
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> ProductCatalogNotifier _productCatalog;
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> CartNotifier _cart;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _productCatalog = ProductCatalogNotifier(ApiService());
    _cart = CartNotifier();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _productCatalog.fetchProducts();
    });
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _productCatalog.dispose();
    _cart.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: Text(<span class="hljs-string">'Product Catalog'</span>)),
      body: MultiValueListenableBuilder(
        valueListenables: [
          _productCatalog.isLoading,
          _productCatalog.error,
          _productCatalog.products,
        ],
        builder: (context, (<span class="hljs-built_in">bool</span> isLoading, <span class="hljs-built_in">String?</span> error, <span class="hljs-built_in">List</span>&lt;Product&gt; products), _) {
          <span class="hljs-keyword">return</span> <span class="hljs-keyword">switch</span> ((isLoading, error, products)) {
            (<span class="hljs-keyword">true</span>, _, _) =&gt; Center(child: CircularProgressIndicator()),
            (_, <span class="hljs-built_in">String</span> error, _) =&gt; _buildErrorWidget(error),
            (_, _, []) =&gt; Center(child: Text(<span class="hljs-string">'No products available'</span>)),
            (_, _, <span class="hljs-built_in">List</span>&lt;Product&gt; products) =&gt; _buildProductList(products),
          };
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () =&gt; Navigator.push(
          context,
          MaterialPageRoute(builder: (_) =&gt; CartPage(_cart)),
        ),
        child: Icon(Icons.shopping_cart),
      ),
    );
  }

  Widget _buildErrorWidget(<span class="hljs-built_in">String</span> error) {
    <span class="hljs-keyword">return</span> Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(error),
          ElevatedButton(
            onPressed: () =&gt; _productCatalog.fetchProducts(),
            child: Text(<span class="hljs-string">'Retry'</span>),
          ),
        ],
      ),
    );
  }

  Widget _buildProductList(<span class="hljs-built_in">List</span>&lt;Product&gt; products) {
    <span class="hljs-keyword">return</span> RefreshIndicator(
      onRefresh: () =&gt; _productCatalog.fetchProducts(),
      child: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          <span class="hljs-keyword">final</span> product = products[index];
          <span class="hljs-keyword">return</span> ListTile(
            title: Text(product.name),
            subtitle: Text(<span class="hljs-string">'\$<span class="hljs-subst">${product.price.toStringAsFixed(<span class="hljs-number">2</span>)}</span>'</span>),
            trailing: IconButton(
              icon: Icon(Icons.add_shopping_cart),
              onPressed: () =&gt; _cart.addItem(product),
            ),
          );
        },
      ),
    );
  }
}
</code></pre>
<p>Let's break down this implementation:</p>
<ol>
<li><p>We use a StatefulWidget to manage the lifecycle of our notifiers.</p>
</li>
<li><p>In <code>initState</code>, we initialize our notifiers and fetch products after the widget is built.</p>
</li>
<li><p>In <code>dispose</code>, we make sure to dispose of our notifiers to prevent memory leaks.</p>
</li>
<li><p>We use <code>MultiValueListenableBuilder</code> to listen to multiple ValueNotifiers simultaneously.</p>
</li>
<li><p>We use Dart's pattern matching feature to handle different states elegantly: Remember that this pattern matching syntax requires Dart 3.0 or later. If you're using an earlier version of Dart, you'll need to stick with the if-else approach or update your Dart SDK.</p>
<ul>
<li><p><code>(true, , )</code> matches when loading.</p>
</li>
<li><p><code>(_, String error, _)</code> matches when there's an error.</p>
</li>
<li><p><code>(_, _, [])</code> matches when the product list is empty.</p>
</li>
<li><p><code>(_, _, List&lt;Product&gt; products)</code> matches when we have products to display.</p>
</li>
</ul>
</li>
<li><p>We extract the error widget and product list widget into separate methods for better organization.</p>
</li>
<li><p>We use a <code>RefreshIndicator</code> to allow users to manually refresh the product list.</p>
</li>
</ol>
<p>Using <code>Listenable.merge</code> constructor:</p>
<pre><code class="lang-dart">ListenableBuilder(
        listenable: Listenable.merge([
          _productCatalog.loadingStatus,
          _productCatalog.errorMessage,
          _productCatalog.products,
        ]),
        builder: (context, _) {
          <span class="hljs-keyword">final</span> status = _productCatalog.loadingStatus.value;
          <span class="hljs-keyword">final</span> errorMessage = _productCatalog.errorMessage.value;
          <span class="hljs-keyword">final</span> products = _productCatalog.products.value;

          <span class="hljs-keyword">return</span> <span class="hljs-keyword">switch</span> ((status, errorMessage, products)) {
            (LoadingStatus.loading, _, _) =&gt; 
              Center(child: CircularProgressIndicator()),
            (LoadingStatus.error, <span class="hljs-built_in">String</span> error, _) =&gt; 
              Center(child: Text(error)),
            (_, _, <span class="hljs-built_in">List</span>&lt;Product&gt; products) =&gt; ListView.builder(
              itemCount: products.length,
              itemBuilder: (context, index) {
                <span class="hljs-keyword">final</span> product = products[index];
                <span class="hljs-keyword">return</span> ListTile(
                  title: Text(product.name),
                  subtitle: Text(<span class="hljs-string">'\$<span class="hljs-subst">${product.price.toStringAsFixed(<span class="hljs-number">2</span>)}</span>'</span>),
                );
              },
            ),
            _ =&gt; Center(child: Text(<span class="hljs-string">'Unexpected state'</span>)),
          };
        },
      )
</code></pre>
<p>Now for the <code>CartPage</code> which will show our shopping cart.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CartPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">final</span> CartNotifier cart;

  <span class="hljs-keyword">const</span> CartPage({Key? key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.cart}) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(<span class="hljs-string">'Shopping Cart'</span>),
      ),
      body: ValueListenableBuilder&lt;<span class="hljs-built_in">List</span>&lt;CartItem&gt;&gt;(
        valueListenable: cart.items,
        builder: (context, items, child) {
          <span class="hljs-keyword">if</span> (items.isEmpty) {
            <span class="hljs-keyword">return</span> Center(child: Text(<span class="hljs-string">'Your cart is empty'</span>));
          }
          <span class="hljs-keyword">return</span> ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) {
              <span class="hljs-keyword">final</span> item = items[index];
              <span class="hljs-keyword">return</span> ListTile(
                title: Text(item.product.name),
                subtitle: Text(<span class="hljs-string">'\$<span class="hljs-subst">${item.product.price.toStringAsFixed(<span class="hljs-number">2</span>)}</span>'</span>),
                trailing: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    IconButton(
                      icon: Icon(Icons.remove),
                      onPressed: () =&gt; cart.decreaseQuantity(item.product),
                    ),
                    ValueListenableBuilder&lt;<span class="hljs-built_in">int</span>&gt;(
                      valueListenable: item.quantity,
                      builder: (context, quantity, child) {
                        <span class="hljs-keyword">return</span> Text(<span class="hljs-string">'<span class="hljs-subst">$quantity</span>'</span>);
                      },
                    ),
                    IconButton(
                      icon: Icon(Icons.add),
                      onPressed: () =&gt; cart.addItem(item.product),
                    ),
                  ],
                ),
              );
            },
          );
        },
      ),
      bottomNavigationBar: ValueListenableBuilder&lt;<span class="hljs-built_in">double</span>&gt;(
        valueListenable: cart.totalPrice,
        builder: (context, totalPrice, child) {
          <span class="hljs-keyword">return</span> Padding(
            padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
            child: Text(
              <span class="hljs-string">'Total: \$<span class="hljs-subst">${totalPrice.toStringAsFixed(<span class="hljs-number">2</span>)}</span>'</span>,
              style: Theme.of(context).textTheme.headline6,
              textAlign: TextAlign.center,
            ),
          );
        },
      ),
    );
  }
}
</code></pre>
<h3 id="heading-5-unit-tests">5. Unit Tests</h3>
<p>You might be wondering how to test your <code>ValueNotifier</code>.</p>
<p>Let’s test the <code>ProductCatalogNotifier</code>.</p>
<p>Our tests should cover:</p>
<ul>
<li><p>Initial state of the product catalog</p>
</li>
<li><p>Successful fetching of products</p>
</li>
<li><p>Handling of errors during fetching</p>
</li>
<li><p>Loading state during fetching</p>
</li>
<li><p>Clearing of previous errors on new fetch</p>
</li>
<li><p>Replacement of previous products on new fetch</p>
</li>
</ul>
<p>To write our test, we will use <code>Mockito</code>. <code>Mockito</code> is a popular mocking framework for Dart and Flutter. It allows you to create mock objects for your tests, which is particularly useful when you want to test classes that depend on other classes or services.</p>
<ol>
<li>Add <code>Mockito</code> and <code>build_runner</code> to your <code>pubspec.yaml</code> file under <code>dev_dependencies</code> to your project:</li>
</ol>
<pre><code class="lang-yaml"><span class="hljs-attr">dev_dependencies:</span>
  <span class="hljs-attr">flutter_test:</span>
    <span class="hljs-attr">sdk:</span> <span class="hljs-string">flutter</span>
  <span class="hljs-attr">mockito:</span> <span class="hljs-string">^5.0.0</span>
  <span class="hljs-attr">build_runner:</span> <span class="hljs-string">^2.0.0</span>
</code></pre>
<p>Then run <code>flutter pub get</code> to install these packages.</p>
<ol start="2">
<li>Create a test file:</li>
</ol>
<p>Create a new file for your tests, e.g., <code>test/product_catalog_notifier_test.dart</code>.<br />We’ll use <code>GenerateNiceMocks</code> to generate mocks for our <code>ApiService</code> class. <code>GenerateNiceMocks</code> is a newer and often more convenient way to generate mocks using Mockito. It creates "nice" mocks by default, which means they return null for unexpected method calls instead of throwing exceptions. This can be helpful in many testing scenarios.</p>
<pre><code class="lang-dart"><span class="hljs-meta">@GenerateNiceMocks</span>([MockSpec&lt;ApiService&gt;()])
<span class="hljs-keyword">import</span> <span class="hljs-string">'product_catalog_notifier_test.mocks.dart'</span>;

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">late</span> ProductCatalogNotifier productCatalog;
  <span class="hljs-keyword">late</span> MockApiService mockApiService;

  setUp(() {
    mockApiService = MockApiService();
    productCatalog = ProductCatalogNotifier(mockApiService);
  });

  tearDown(() {
    productCatalog.dispose();
  });

  group(<span class="hljs-string">'ProductCatalogNotifier'</span>, () {
    test(<span class="hljs-string">'initial state is empty'</span>, () {
      expect(productCatalog.products.value, isEmpty);
      expect(productCatalog.isLoading.value, <span class="hljs-keyword">false</span>);
      expect(productCatalog.error.value, isNull);
    });

    test(<span class="hljs-string">'fetchProducts updates products on success'</span>, () <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> mockProducts = [
        Product(id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Test Product 1'</span>, price: <span class="hljs-number">10.0</span>, imageUrl: <span class="hljs-string">'test1.jpg'</span>),
        Product(id: <span class="hljs-number">2</span>, name: <span class="hljs-string">'Test Product 2'</span>, price: <span class="hljs-number">20.0</span>, imageUrl: <span class="hljs-string">'test2.jpg'</span>),
      ];

      when(mockApiService.fetchProducts()).thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; mockProducts);

      <span class="hljs-keyword">await</span> productCatalog.fetchProducts();

      expect(productCatalog.products.value, equals(mockProducts));
      expect(productCatalog.isLoading.value, <span class="hljs-keyword">false</span>);
      expect(productCatalog.error.value, isNull);
    });

    test(<span class="hljs-string">'fetchProducts updates error on failure'</span>, () <span class="hljs-keyword">async</span> {
      when(mockApiService.fetchProducts()).thenThrow(Exception(<span class="hljs-string">'API error'</span>));

      <span class="hljs-keyword">await</span> productCatalog.fetchProducts();

      expect(productCatalog.products.value, isEmpty);
      expect(productCatalog.isLoading.value, <span class="hljs-keyword">false</span>);
      expect(productCatalog.error.value, contains(<span class="hljs-string">'Failed to fetch products'</span>));
    });

    test(<span class="hljs-string">'isLoading is true while fetching products'</span>, () <span class="hljs-keyword">async</span> {
      when(mockApiService.fetchProducts()).thenAnswer((_) <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">100</span>));
        <span class="hljs-keyword">return</span> [];
      });

      <span class="hljs-keyword">final</span> future = productCatalog.fetchProducts();

      expect(productCatalog.isLoading.value, <span class="hljs-keyword">true</span>);

      <span class="hljs-keyword">await</span> future;

      expect(productCatalog.isLoading.value, <span class="hljs-keyword">false</span>);
    });

    test(<span class="hljs-string">'fetchProducts clears previous error'</span>, () <span class="hljs-keyword">async</span> {
      <span class="hljs-comment">// First, simulate an error</span>
      when(mockApiService.fetchProducts()).thenThrow(Exception(<span class="hljs-string">'API error'</span>));
      <span class="hljs-keyword">await</span> productCatalog.fetchProducts();
      expect(productCatalog.error.value, isNotNull);

      <span class="hljs-comment">// Then, simulate a successful fetch</span>
      when(mockApiService.fetchProducts()).thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; []);
      <span class="hljs-keyword">await</span> productCatalog.fetchProducts();
      expect(productCatalog.error.value, isNull);
    });

    test(<span class="hljs-string">'fetchProducts replaces previous products'</span>, () <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> mockProducts1 = [
        Product(id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Test Product 1'</span>, price: <span class="hljs-number">10.0</span>, imageUrl: <span class="hljs-string">'test1.jpg'</span>),
      ];
      <span class="hljs-keyword">final</span> mockProducts2 = [
        Product(id: <span class="hljs-number">2</span>, name: <span class="hljs-string">'Test Product 2'</span>, price: <span class="hljs-number">20.0</span>, imageUrl: <span class="hljs-string">'test2.jpg'</span>),
      ];

      when(mockApiService.fetchProducts()).thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; mockProducts1);
      <span class="hljs-keyword">await</span> productCatalog.fetchProducts();
      expect(productCatalog.products.value, equals(mockProducts1));

      when(mockApiService.fetchProducts()).thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; mockProducts2);
      <span class="hljs-keyword">await</span> productCatalog.fetchProducts();
      expect(productCatalog.products.value, equals(mockProducts2));
    });
  });
}
</code></pre>
<p>Remember to run <code>flutter pub run build_runner build</code></p>
<p>Let’s test our <code>CartNotifier</code>.<br />We’ll have a test suite that covers</p>
<ul>
<li><p>Initial state of the cart</p>
</li>
<li><p>Adding a new item to an empty cart</p>
</li>
<li><p>Increasing quantity of an existing item</p>
</li>
<li><p>Adding different items separately</p>
</li>
<li><p>Removing an item from the cart</p>
</li>
<li><p>Attempting to remove a non-existent item</p>
</li>
<li><p>Updating total price with multiple operations</p>
</li>
<li><p>Edge case of adding an item with quantity 0 and then incrementing</p>
</li>
<li><p>Removing the last item and ensuring total price is 0</p>
</li>
<li><p>Handling high-priced items without precision errors</p>
</li>
</ul>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">late</span> CartNotifier cartNotifier;
  <span class="hljs-keyword">late</span> Product product1;
  <span class="hljs-keyword">late</span> Product product2;

  setUp(() {
    cartNotifier = CartNotifier();
    product1 = Product(id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Test Product 1'</span>, price: <span class="hljs-number">10.0</span>, imageUrl: <span class="hljs-string">'test1.jpg'</span>);
    product2 = Product(id: <span class="hljs-number">2</span>, name: <span class="hljs-string">'Test Product 2'</span>, price: <span class="hljs-number">20.0</span>, imageUrl: <span class="hljs-string">'test2.jpg'</span>);
  });

  tearDown(() {
    cartNotifier.dispose();
  });

  group(<span class="hljs-string">'CartNotifier'</span>, () {
    test(<span class="hljs-string">'initial state is empty'</span>, () {
      expect(cartNotifier.items.value, isEmpty);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">0.0</span>);
    });

    test(<span class="hljs-string">'addItem adds new item to empty cart'</span>, () {
      cartNotifier.addItem(product1);
      expect(cartNotifier.items.value.length, <span class="hljs-number">1</span>);
      expect(cartNotifier.items.value[<span class="hljs-number">0</span>].product, product1);
      expect(cartNotifier.items.value[<span class="hljs-number">0</span>].quantity.value, <span class="hljs-number">1</span>);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">10.0</span>);
    });

    test(<span class="hljs-string">'addItem increases quantity for existing item'</span>, () {
      cartNotifier.addItem(product1);
      cartNotifier.addItem(product1);
      expect(cartNotifier.items.value.length, <span class="hljs-number">1</span>);
      expect(cartNotifier.items.value[<span class="hljs-number">0</span>].quantity.value, <span class="hljs-number">2</span>);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">20.0</span>);
    });

    test(<span class="hljs-string">'addItem adds different items separately'</span>, () {
      cartNotifier.addItem(product1);
      cartNotifier.addItem(product2);
      expect(cartNotifier.items.value.length, <span class="hljs-number">2</span>);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">30.0</span>);
    });

    test(<span class="hljs-string">'removeItem removes the item from the cart'</span>, () {
      cartNotifier.addItem(product1);
      cartNotifier.addItem(product2);
      cartNotifier.removeItem(product1);
      expect(cartNotifier.items.value.length, <span class="hljs-number">1</span>);
      expect(cartNotifier.items.value[<span class="hljs-number">0</span>].product, product2);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">20.0</span>);
    });

    test(<span class="hljs-string">'removeItem does nothing if item not in cart'</span>, () {
      cartNotifier.addItem(product1);
      cartNotifier.removeItem(product2);
      expect(cartNotifier.items.value.length, <span class="hljs-number">1</span>);
      expect(cartNotifier.items.value[<span class="hljs-number">0</span>].product, product1);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">10.0</span>);
    });

    test(<span class="hljs-string">'totalPrice updates correctly with multiple operations'</span>, () {
      cartNotifier.addItem(product1);
      cartNotifier.addItem(product2);
      cartNotifier.addItem(product1);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">40.0</span>);
      cartNotifier.removeItem(product2);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">20.0</span>);
    });

    test(<span class="hljs-string">'adding item with quantity 0 and then incrementing works correctly'</span>, () {
      cartNotifier.addItem(product1);
      cartNotifier.items.value[<span class="hljs-number">0</span>].quantity.value = <span class="hljs-number">0</span>;
      cartNotifier.addItem(product1);
      expect(cartNotifier.items.value.length, <span class="hljs-number">1</span>);
      expect(cartNotifier.items.value[<span class="hljs-number">0</span>].quantity.value, <span class="hljs-number">1</span>);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">10.0</span>);
    });

    test(<span class="hljs-string">'removing last item sets total price to 0'</span>, () {
      cartNotifier.addItem(product1);
      cartNotifier.removeItem(product1);
      expect(cartNotifier.items.value, isEmpty);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">0.0</span>);
    });

    test(<span class="hljs-string">'adding item with very high price doesn\'t cause precision errors'</span>, () {
      <span class="hljs-keyword">final</span> expensiveProduct = Product(id: <span class="hljs-number">3</span>, name: <span class="hljs-string">'Expensive'</span>, price: <span class="hljs-number">1000000.01</span>, imageUrl: <span class="hljs-string">'expensive.jpg'</span>);
      cartNotifier.addItem(expensiveProduct);
      expect(cartNotifier.totalPrice.value, <span class="hljs-number">1000000.01</span>);
    });
  });
}
</code></pre>
<p>Next, let’s add a widget test our <code>ProductListPage</code>. We are testing several scenarios:</p>
<ul>
<li><p>Loading state when fetching products</p>
</li>
<li><p>Error state when fetching fails</p>
</li>
<li><p>Successful display of products</p>
</li>
<li><p>Adding a product to the cart</p>
</li>
<li><p>Navigating to the cart page</p>
</li>
</ul>
<pre><code class="lang-dart"><span class="hljs-meta">@GenerateNiceMocks</span>([MockSpec&lt;ApiService&gt;(), MockSpec&lt;CartNotifier&gt;()])
<span class="hljs-keyword">import</span> <span class="hljs-string">'product_list_page_test.mocks.dart'</span>;

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">late</span> MockApiService mockApiService;
  <span class="hljs-keyword">late</span> MockCartNotifier mockCartNotifier;
  <span class="hljs-keyword">late</span> ProductCatalogNotifier productCatalogNotifier;

  setUp(() {
    mockApiService = MockApiService();
    mockCartNotifier = MockCartNotifier();
    productCatalogNotifier = ProductCatalogNotifier(mockApiService);
  });

  Widget createWidgetUnderTest() {
    <span class="hljs-keyword">return</span> MaterialApp(
      home: ProductListPage(
        productCatalog: productCatalogNotifier,
        cart: mockCartNotifier,
      ),
    );
  }

  group(<span class="hljs-string">'ProductListPage'</span>, () {
    testWidgets(<span class="hljs-string">'displays loading indicator when fetching products'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
      when(mockApiService.fetchProducts()).thenAnswer((_) <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>));
        <span class="hljs-keyword">return</span> [];
      });

      <span class="hljs-keyword">await</span> tester.pumpWidget(createWidgetUnderTest());

      expect(find.byType(CircularProgressIndicator), findsOneWidget);
    });

    testWidgets(<span class="hljs-string">'displays error message when fetch fails'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
      when(mockApiService.fetchProducts()).thenThrow(Exception(<span class="hljs-string">'Failed to fetch products'</span>));

      <span class="hljs-keyword">await</span> tester.pumpWidget(createWidgetUnderTest());
      <span class="hljs-keyword">await</span> tester.pumpAndSettle();

      expect(find.text(<span class="hljs-string">'Error: Failed to fetch products'</span>), findsOneWidget);
    });

    testWidgets(<span class="hljs-string">'displays list of products when fetch succeeds'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> mockProducts = [
        Product(id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Test Product 1'</span>, price: <span class="hljs-number">10.0</span>, imageUrl: <span class="hljs-string">'test1.jpg'</span>),
        Product(id: <span class="hljs-number">2</span>, name: <span class="hljs-string">'Test Product 2'</span>, price: <span class="hljs-number">20.0</span>, imageUrl: <span class="hljs-string">'test2.jpg'</span>),
      ];

      when(mockApiService.fetchProducts()).thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; mockProducts);

      <span class="hljs-keyword">await</span> tester.pumpWidget(createWidgetUnderTest());
      <span class="hljs-keyword">await</span> tester.pumpAndSettle();

      expect(find.text(<span class="hljs-string">'Test Product 1'</span>), findsOneWidget);
      expect(find.text(<span class="hljs-string">'Test Product 2'</span>), findsOneWidget);
      expect(find.text(<span class="hljs-string">'\$10.00'</span>), findsOneWidget);
      expect(find.text(<span class="hljs-string">'\$20.00'</span>), findsOneWidget);
    });

    testWidgets(<span class="hljs-string">'adds product to cart when add button is tapped'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> mockProducts = [
        Product(id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Test Product'</span>, price: <span class="hljs-number">10.0</span>, imageUrl: <span class="hljs-string">'test.jpg'</span>),
      ];

      when(mockApiService.fetchProducts()).thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; mockProducts);

      <span class="hljs-keyword">await</span> tester.pumpWidget(createWidgetUnderTest());
      <span class="hljs-keyword">await</span> tester.pumpAndSettle();

      <span class="hljs-keyword">await</span> tester.tap(find.byIcon(Icons.add_shopping_cart));
      <span class="hljs-keyword">await</span> tester.pump();

      verify(mockCartNotifier.addItem(mockProducts[<span class="hljs-number">0</span>])).called(<span class="hljs-number">1</span>);
    });

    testWidgets(<span class="hljs-string">'navigates to cart page when cart icon is tapped'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
      when(mockApiService.fetchProducts()).thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; []);

      <span class="hljs-keyword">await</span> tester.pumpWidget(createWidgetUnderTest());
      <span class="hljs-keyword">await</span> tester.pumpAndSettle();

      <span class="hljs-keyword">await</span> tester.tap(find.byIcon(Icons.shopping_cart));
      <span class="hljs-keyword">await</span> tester.pumpAndSettle();

      <span class="hljs-comment">// Verify navigation to CartPage</span>
      <span class="hljs-comment">// This depends on how you've set up navigation. You might need to adjust this.</span>
      expect(find.byType(CartPage), findsOneWidget);
    });
  });
}
</code></pre>
<p>We're using <code>tester.pumpAndSettle()</code> to wait for all animations to complete and the widget tree to settle. These tests cover the main functionality of the <code>ProductListPage</code>, including loading states, error handling, displaying products, adding to cart, and navigation. They help ensure that your UI behaves correctly under various conditions and that user interactions produce the expected results.</p>
<p>Remember to update these tests as you add new features or change the behavior of your <code>ProductListPage</code>. Widget tests are a powerful tool for maintaining the quality and reliability of your Flutter application's UI.</p>
<p>Finally, let’s add a widget test for our <code>CartPage</code></p>
<pre><code class="lang-dart"><span class="hljs-meta">@GenerateNiceMocks</span>([MockSpec&lt;CartNotifier&gt;()])
<span class="hljs-keyword">import</span> <span class="hljs-string">'cart_page_test.mocks.dart'</span>;

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">late</span> MockCartNotifier mockCartNotifier;

  setUp(() {
    mockCartNotifier = MockCartNotifier();
  });

  Widget createWidgetUnderTest() {
    <span class="hljs-keyword">return</span> MaterialApp(
      home: CartPage(cart: mockCartNotifier),
    );
  }

  group(<span class="hljs-string">'CartPage'</span>, () {
    testWidgets(<span class="hljs-string">'displays empty cart message when cart is empty'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
      when(mockCartNotifier.items).thenReturn(ValueNotifier([]));
      when(mockCartNotifier.totalPrice).thenReturn(ValueNotifier(<span class="hljs-number">0.0</span>));

      <span class="hljs-keyword">await</span> tester.pumpWidget(createWidgetUnderTest());

      expect(find.text(<span class="hljs-string">'Your cart is empty'</span>), findsOneWidget);
    });

    testWidgets(<span class="hljs-string">'displays cart items when cart is not empty'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> product = Product(id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Test Product'</span>, price: <span class="hljs-number">10.0</span>, imageUrl: <span class="hljs-string">'test.jpg'</span>);
      <span class="hljs-keyword">final</span> cartItem = CartItem(product: product, quantity: ValueNotifier(<span class="hljs-number">2</span>));

      when(mockCartNotifier.items).thenReturn(ValueNotifier([cartItem]));
      when(mockCartNotifier.totalPrice).thenReturn(ValueNotifier(<span class="hljs-number">20.0</span>));

      <span class="hljs-keyword">await</span> tester.pumpWidget(createWidgetUnderTest());

      expect(find.text(<span class="hljs-string">'Test Product'</span>), findsOneWidget);
      expect(find.text(<span class="hljs-string">'\$10.00'</span>), findsOneWidget);
      expect(find.text(<span class="hljs-string">'2'</span>), findsOneWidget);
      expect(find.text(<span class="hljs-string">'Total: \$20.00'</span>), findsOneWidget);
    });

    testWidgets(<span class="hljs-string">'increases quantity when add button is tapped'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> product = Product(id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Test Product'</span>, price: <span class="hljs-number">10.0</span>, imageUrl: <span class="hljs-string">'test.jpg'</span>);
      <span class="hljs-keyword">final</span> cartItem = CartItem(product: product, quantity: ValueNotifier(<span class="hljs-number">1</span>));

      when(mockCartNotifier.items).thenReturn(ValueNotifier([cartItem]));
      when(mockCartNotifier.totalPrice).thenReturn(ValueNotifier(<span class="hljs-number">10.0</span>));

      <span class="hljs-keyword">await</span> tester.pumpWidget(createWidgetUnderTest());

      <span class="hljs-keyword">await</span> tester.tap(find.byIcon(Icons.add));
      <span class="hljs-keyword">await</span> tester.pump();

      verify(mockCartNotifier.addItem(product)).called(<span class="hljs-number">1</span>);
    });

    testWidgets(<span class="hljs-string">'decreases quantity when remove button is tapped'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> product = Product(id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Test Product'</span>, price: <span class="hljs-number">10.0</span>, imageUrl: <span class="hljs-string">'test.jpg'</span>);
      <span class="hljs-keyword">final</span> cartItem = CartItem(product: product, quantity: ValueNotifier(<span class="hljs-number">2</span>));

      when(mockCartNotifier.items).thenReturn(ValueNotifier([cartItem]));
      when(mockCartNotifier.totalPrice).thenReturn(ValueNotifier(<span class="hljs-number">20.0</span>));

      <span class="hljs-keyword">await</span> tester.pumpWidget(createWidgetUnderTest());

      <span class="hljs-keyword">await</span> tester.tap(find.byIcon(Icons.remove));
      <span class="hljs-keyword">await</span> tester.pump();

      verify(mockCartNotifier.decreaseQuantity(product)).called(<span class="hljs-number">1</span>);
    });
  });
}
</code></pre>
<p>These tests cover the main functionality of the <code>CartPage</code>, including displaying cart items, showing the total price, and handling quantity changes.</p>
<p>Remember to run <code>flutter pub run build_runner build</code> to generate the mock classes before running the tests.</p>
<h2 id="heading-lets-wrap-up">Let’s Wrap Up</h2>
<p><code>ValueNotifier</code> and <code>ValueListenableBuilder</code> provide a powerful yet simple way to manage state in Flutter applications. By using these tools along with custom widgets like <code>MultiValueListenableBuilder</code> and Dart's pattern matching feature, we can create responsive, efficient, and easy-to-maintain applications.</p>
<p>Remember to always dispose of your <code>ValueNotifier</code>s when they're no longer needed, and consider breaking down complex widgets into smaller, more manageable pieces. With these techniques, you can handle even complex state management scenarios with ease in your Flutter applications.</p>
<p>NOTE:<br />There are scenarios where ChangeNotifier might be more appropriate:</p>
<ul>
<li><p>Complex State: When your state becomes more complex with multiple interdependent properties, ChangeNotifier can provide a more organized way to manage and update this state.</p>
</li>
<li><p>Multiple Listeners: If you need to notify multiple widgets about state changes and these changes might affect multiple properties at once, ChangeNotifier can be more efficient.</p>
</li>
<li><p>Business Logic: When you have significant business logic associated with your state updates, ChangeNotifier allows you to encapsulate this logic within the notifier class.</p>
</li>
<li><p>Computed Properties: If you need computed properties that depend on multiple pieces of state, ChangeNotifier can handle this more elegantly.</p>
</li>
<li><p>State Persistence: For cases where you need to persist state across app restarts or sync with external storage, ChangeNotifier can provide a centralized place to manage this.</p>
</li>
<li><p>Larger Applications: In larger applications with more complex state management needs, ChangeNotifier (often used with Provider) can offer a more scalable solution.</p>
</li>
</ul>
<p>That said, it's important to note that you don't always need to jump to <code>ChangeNotifier</code>. For many cases, especially in smaller widgets or components, <code>ValueNotifier</code> and <code>ValueListenableBuilder</code> (or <code>MultiValueListenableBuilder</code>) can provide a simpler and more focused solution. The key is to choose the right tool for the job based on the complexity of your state management needs.</p>
]]></content:encoded></item><item><title><![CDATA[Comprehensive Guide to Testing Riverpod Providers]]></title><description><![CDATA[Introduction to Riverpod
Riverpod is a reactive Caching and Data-binding Framework which can be used as powerful state management library for Flutter and Dart applications, created by Remi Rousselet, the author of the popular Provider package. It add...]]></description><link>https://article.temiajiboye.com/comprehensive-guide-to-testing-riverpod-providers</link><guid isPermaLink="true">https://article.temiajiboye.com/comprehensive-guide-to-testing-riverpod-providers</guid><dc:creator><![CDATA[Temitope Ajiboye]]></dc:creator><pubDate>Wed, 21 Aug 2024 22:34:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724279550166/faa088a5-9282-4707-bb1b-b03af211db2c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction-to-riverpod">Introduction to Riverpod</h2>
<p>Riverpod is a reactive Caching and Data-binding Framework which can be used as powerful state management library for Flutter and Dart applications, created by Remi Rousselet, the author of the popular Provider package. It addresses several limitations of Provider and offers significant advantages:</p>
<ol>
<li><strong>Compile-time safety</strong>: Riverpod catches many errors at compile-time rather than runtime.</li>
</ol>
<ol start="2">
<li><strong>Testability</strong>: It provides excellent support for unit testing and widget testing.</li>
</ol>
<ol start="3">
<li><p><strong>Flexibility</strong>: Riverpod allows for easy creation and combination of providers.</p>
</li>
<li><p><strong>Improved performance</strong>: It optimizes rebuilds and allows for more granular control over state updates.</p>
</li>
</ol>
<p>Riverpod is designed to work seamlessly with the Flutter framework but can also be used in pure Dart applications. It provides a set of tools to manage application state, from simple values to complex asynchronous data flows.</p>
<h2 id="heading-providercontainer-and-testing-utilities">ProviderContainer and Testing Utilities</h2>
<p>Before diving into specific provider types, it's crucial to understand the tools Riverpod offers for testing.</p>
<h3 id="heading-providercontainer">ProviderContainer</h3>
<p>ProviderContainer is a fundamental class in Riverpod for testing purposes. It allows you to create an isolated environment for your providers, separate from the widget tree. This isolation is essential for unit testing providers without the need for a full widget test setup.</p>
<p>Key points about ProviderContainer:</p>
<ol>
<li><p>It holds the state of all providers created within it.</p>
</li>
<li><p>It allows you to read and interact with providers outside of a widget context.</p>
</li>
<li><p>It supports overrides, enabling you to replace providers with mocks or different implementations for testing.</p>
</li>
</ol>
<h3 id="heading-createcontainer-utility">createContainer Utility</h3>
<p>To streamline the creation of <em>ProviderContainer</em> instances in tests, you can use a utility function like <em>createContainer</em>:</p>
<pre><code class="lang-dart">ProviderContainer createContainer({
  ProviderContainer? parent,
  <span class="hljs-built_in">List</span>&lt;Override&gt; overrides = <span class="hljs-keyword">const</span> [],
  <span class="hljs-built_in">List</span>&lt;ProviderObserver&gt;? observers,
}) {
  <span class="hljs-keyword">final</span> container = ProviderContainer(
    parent: parent,
    overrides: overrides,
    observers: observers,
  );

  addTearDown(container.dispose);

  <span class="hljs-keyword">return</span> container;
}
</code></pre>
<p>This utility function:</p>
<ol>
<li><p>Creates a new ProviderContainer with optional parameters for parent, overrides, and observers.</p>
</li>
<li><p>Automatically adds a teardown step to dispose of the container after the test, preventing memory leaks.</p>
</li>
<li><p>Provides a consistent way to create containers across your test suite.</p>
</li>
</ol>
<h3 id="heading-overrides-in-riverpod-testing">Overrides in Riverpod Testing</h3>
<p>Overrides are a powerful feature in Riverpod that allow you to replace the implementation of a provider for testing purposes. This is particularly useful for:</p>
<ul>
<li><p>Mocking dependencies</p>
</li>
<li><p>Testing different scenarios by providing different initial values</p>
</li>
<li><p>Isolating parts of your application for focused testing</p>
</li>
</ul>
<h2 id="heading-types-of-riverpod-providers-and-how-to-test-them">Types of Riverpod Providers and How to Test Them</h2>
<h3 id="heading-1-provider">1. Provider</h3>
<h4 id="heading-what-is-it">What is it?</h4>
<p>Provider is the most basic type of provider in Riverpod. It creates a value that doesn't change over time.</p>
<h4 id="heading-when-to-use-it">When to use it?</h4>
<p>Use Provider for:</p>
<ul>
<li><p>Dependency injection</p>
</li>
<li><p>Computed values that don't change</p>
</li>
<li><p>Singleton instances</p>
</li>
</ul>
<h4 id="heading-testing-provider">Testing Provider</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> apiClientProvider = Provider&lt;ApiClient&gt;((ref) =&gt; ApiClient());

<span class="hljs-keyword">void</span> main() {
  test(<span class="hljs-string">'apiClientProvider returns a singleton instance'</span>, () {
    <span class="hljs-keyword">final</span> container = createContainer();

    <span class="hljs-keyword">final</span> instance1 = container.read(apiClientProvider);
    <span class="hljs-keyword">final</span> instance2 = container.read(apiClientProvider);

    expect(instance1, isA&lt;ApiClient&gt;());
    expect(identical(instance1, instance2), <span class="hljs-keyword">true</span>);
  });
}
</code></pre>
<h4 id="heading-why-test-this-way">Why test this way?</h4>
<ul>
<li><p>We use createContainer to isolate the provider from the rest of the app.</p>
</li>
<li><p>We verify that the provider returns the correct type of object.</p>
</li>
<li><p>We check that multiple reads return the same instance, ensuring it's a singleton.</p>
</li>
</ul>
<h3 id="heading-2-stateprovider">2. StateProvider</h3>
<h4 id="heading-what-is-it-1">What is it?</h4>
<p>StateProvider is used for simple state that can change over time.</p>
<h4 id="heading-when-to-use-it-1">When to use it?</h4>
<p>Use StateProvider for:</p>
<ul>
<li><p>Simple state that can be represented by a single value</p>
</li>
<li><p>State that doesn't require complex logic to update</p>
</li>
</ul>
<h4 id="heading-testing-stateprovider">Testing StateProvider</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> counterStateProvider = StateProvider&lt;<span class="hljs-built_in">int</span>&gt;((ref) =&gt; <span class="hljs-number">0</span>);

<span class="hljs-keyword">void</span> main() {
  test(<span class="hljs-string">'counterStateProvider can be incremented and reset'</span>, () {
    <span class="hljs-keyword">final</span> container = createContainer();

    expect(container.read(counterStateProvider), <span class="hljs-number">0</span>);

    container.read(counterStateProvider.notifier).state++;
    expect(container.read(counterStateProvider), <span class="hljs-number">1</span>);

    container.read(counterStateProvider.notifier).state = <span class="hljs-number">0</span>;
    expect(container.read(counterStateProvider), <span class="hljs-number">0</span>);
  });
}
</code></pre>
<h4 id="heading-why-test-this-way-1">Why test this way?</h4>
<ul>
<li><p>We test the initial state to ensure correct initialization.</p>
</li>
<li><p>We modify the state and verify it changes correctly.</p>
</li>
<li><p>We test resetting the state to ensure all state changes work as expected.</p>
</li>
</ul>
<h3 id="heading-3-statenotifierprovider">3. StateNotifierProvider</h3>
<h4 id="heading-what-is-it-2">What is it?</h4>
<p>StateNotifierProvider is used for more complex state management, allowing you to define methods to change the state.</p>
<h4 id="heading-when-to-use-it-2">When to use it?</h4>
<p>Use StateNotifierProvider for:</p>
<ul>
<li><p>Complex state that requires custom logic to update</p>
</li>
<li><p>State that needs to be modified through specific methods</p>
</li>
</ul>
<h4 id="heading-testing-statenotifierprovider">Testing StateNotifierProvider</h4>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Counter</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StateNotifier</span>&lt;<span class="hljs-title">int</span>&gt; </span>{
  Counter() : <span class="hljs-keyword">super</span>(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">void</span> increment() =&gt; state++;
  <span class="hljs-keyword">void</span> decrement() =&gt; state--;
}

<span class="hljs-keyword">final</span> counterNotifierProvider = StateNotifierProvider&lt;Counter, <span class="hljs-built_in">int</span>&gt;((ref) =&gt; Counter());

<span class="hljs-keyword">void</span> main() {
  test(<span class="hljs-string">'counterNotifierProvider handles state changes correctly'</span>, () {
    <span class="hljs-keyword">final</span> container = createContainer();

    expect(container.read(counterNotifierProvider), <span class="hljs-number">0</span>);

    container.read(counterNotifierProvider.notifier).increment();
    expect(container.read(counterNotifierProvider), <span class="hljs-number">1</span>);

    container.read(counterNotifierProvider.notifier).decrement();
    expect(container.read(counterNotifierProvider), <span class="hljs-number">0</span>);
  });
}
</code></pre>
<h4 id="heading-why-test-this-way-2">Why test this way?</h4>
<ul>
<li><p>We test the initial state.</p>
</li>
<li><p>We test each method of the StateNotifier to ensure they modify the state correctly.</p>
</li>
<li><p>We verify that the state is accessible both through the provider and its notifier.</p>
</li>
</ul>
<h3 id="heading-4-notifierprovider">4. NotifierProvider</h3>
<h4 id="heading-what-is-it-3">What is it?</h4>
<p>NotifierProvider is a more recent addition to Riverpod, introduced as an alternative to StateNotifierProvider. It offers a simpler API and better type inference.</p>
<h4 id="heading-when-to-use-it-3">When to use it?</h4>
<p>Use NotifierProvider for:</p>
<ul>
<li><p>Complex state management where you need custom methods to update the state</p>
</li>
<li><p>Situations where you want to avoid the boilerplate of extending StateNotifier</p>
</li>
</ul>
<h4 id="heading-testing-notifierprovider">Testing NotifierProvider</h4>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CounterNotifier</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Notifier</span>&lt;<span class="hljs-title">int</span>&gt; </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">int</span> build() =&gt; <span class="hljs-number">0</span>;

  <span class="hljs-keyword">void</span> increment() =&gt; state++;
  <span class="hljs-keyword">void</span> decrement() =&gt; state--;
}

<span class="hljs-keyword">final</span> counterNotifierProvider = NotifierProvider&lt;CounterNotifier, <span class="hljs-built_in">int</span>&gt;(() =&gt; CounterNotifier());

<span class="hljs-keyword">void</span> main() {
  test(<span class="hljs-string">'counterNotifierProvider handles state changes correctly'</span>, () {
    <span class="hljs-keyword">final</span> container = createContainer();

    expect(container.read(counterNotifierProvider), <span class="hljs-number">0</span>);

    container.read(counterNotifierProvider.notifier).increment();
    expect(container.read(counterNotifierProvider), <span class="hljs-number">1</span>);

    container.read(counterNotifierProvider.notifier).decrement();
    expect(container.read(counterNotifierProvider), <span class="hljs-number">0</span>);
  });
}
</code></pre>
<h4 id="heading-why-test-this-way-3">Why test this way?</h4>
<ul>
<li><p>We test the initial state provided by the build method.</p>
</li>
<li><p>We test each method of the Notifier to ensure they modify the state correctly.</p>
</li>
<li><p>We verify that the state is accessible both through the provider and its notifier.</p>
</li>
</ul>
<h3 id="heading-5-asyncnotifierprovider">5. AsyncNotifierProvider</h3>
<h4 id="heading-what-is-it-4">What is it?</h4>
<p>AsyncNotifierProvider is used for managing asynchronous state with more complex logic. It's similar to NotifierProvider but designed specifically for asynchronous operations.</p>
<h4 id="heading-when-to-use-it-4">When to use it?</h4>
<p>Use AsyncNotifierProvider for:</p>
<ul>
<li><p>Complex asynchronous state management</p>
</li>
<li><p>Scenarios where you need to handle loading, error, and data states</p>
</li>
<li><p>Operations that involve API calls or other asynchronous tasks</p>
</li>
</ul>
<h4 id="heading-testing-asyncnotifierprovider">Testing AsyncNotifierProvider</h4>
<p>Let's create an example AsyncNotifier and then test it:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserAsyncNotifier</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AsyncNotifier</span>&lt;<span class="hljs-title">User</span>&gt; </span>{
  <span class="hljs-meta">@override</span>
  Future&lt;User&gt; build() <span class="hljs-keyword">async</span> {
    <span class="hljs-comment">// Simulating an API call</span>
    <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>));
    <span class="hljs-keyword">return</span> User(<span class="hljs-string">'Initial User'</span>);
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; updateUser(<span class="hljs-built_in">String</span> newName) <span class="hljs-keyword">async</span> {
    state = <span class="hljs-keyword">const</span> AsyncValue.loading();
    <span class="hljs-keyword">try</span> {
      <span class="hljs-comment">// Simulating an API call</span>
      <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>));
      state = AsyncValue.data(User(newName));
    } <span class="hljs-keyword">catch</span> (e, st) {
      state = AsyncValue.error(e, st);
    }
  }
}

<span class="hljs-keyword">final</span> userAsyncNotifierProvider = AsyncNotifierProvider&lt;UserAsyncNotifier, User&gt;(() =&gt; UserAsyncNotifier());

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> name;
  User(<span class="hljs-keyword">this</span>.name);
}

<span class="hljs-comment">// Test</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_test/flutter_test.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:hooks_riverpod/hooks_riverpod.dart'</span>;

<span class="hljs-keyword">void</span> main() {
  test(<span class="hljs-string">'UserAsyncNotifier initializes and updates correctly'</span>, () <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> container = createContainer();

    <span class="hljs-comment">// Test initial state</span>
    expect(container.read(userAsyncNotifierProvider), <span class="hljs-keyword">const</span> AsyncValue&lt;User&gt;.loading());

    <span class="hljs-comment">// Wait for the build method to complete</span>
    <span class="hljs-keyword">await</span> container.read(userAsyncNotifierProvider.future);

    <span class="hljs-comment">// Check the initial user</span>
    <span class="hljs-keyword">final</span> initialUser = container.read(userAsyncNotifierProvider).value;
    expect(initialUser?.name, <span class="hljs-string">'Initial User'</span>);

    <span class="hljs-comment">// Update the user</span>
    <span class="hljs-keyword">final</span> notifier = container.read(userAsyncNotifierProvider.notifier);
    notifier.updateUser(<span class="hljs-string">'New User'</span>);

    <span class="hljs-comment">// Check loading state</span>
    expect(container.read(userAsyncNotifierProvider), <span class="hljs-keyword">const</span> AsyncValue&lt;User&gt;.loading());

    <span class="hljs-comment">// Wait for the update to complete</span>
    <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">2</span>));

    <span class="hljs-comment">// Check the updated user</span>
    <span class="hljs-keyword">final</span> updatedUser = container.read(userAsyncNotifierProvider).value;
    expect(updatedUser?.name, <span class="hljs-string">'New User'</span>);
  });

  test(<span class="hljs-string">'UserAsyncNotifier handles errors'</span>, () <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> container = createContainer();

    <span class="hljs-comment">// Override the provider to simulate an error</span>
    <span class="hljs-keyword">final</span> errorProvider = AsyncNotifierProvider&lt;UserAsyncNotifier, User&gt;(() {
      <span class="hljs-keyword">return</span> UserAsyncNotifier()
        ..updateUser = (<span class="hljs-built_in">String</span> newName) <span class="hljs-keyword">async</span> {
          <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Update failed'</span>);
        };
    });

    <span class="hljs-comment">// Wait for the initial build</span>
    <span class="hljs-keyword">await</span> container.read(errorProvider.future);

    <span class="hljs-comment">// Attempt to update, which should cause an error</span>
    <span class="hljs-keyword">final</span> notifier = container.read(errorProvider.notifier);
    <span class="hljs-keyword">await</span> notifier.updateUser(<span class="hljs-string">'Error User'</span>);

    <span class="hljs-comment">// Check that the state is an error</span>
    <span class="hljs-keyword">final</span> errorState = container.read(errorProvider);
    expect(errorState, isA&lt;AsyncError&gt;());
    expect(errorState.error, isA&lt;Exception&gt;());
    expect((errorState.error <span class="hljs-keyword">as</span> Exception).toString(), <span class="hljs-string">'Exception: Update failed'</span>);
  });
}
</code></pre>
<h4 id="heading-why-test-this-way-4">Why test this way?</h4>
<ul>
<li><p>We test the initial loading state and the result of the build method.</p>
</li>
<li><p>We verify that the updateUser method correctly transitions through loading and data states.</p>
</li>
<li><p>We test error handling by overriding the provider to simulate an error condition.</p>
</li>
<li><p>We check all possible states: loading, data, and error.</p>
</li>
</ul>
<h4 id="heading-key-points-for-testing-asyncnotifierprovider">Key Points for Testing AsyncNotifierProvider</h4>
<ul>
<li><p><strong>Initial State</strong>: Always check the initial loading state before the build method completes.</p>
</li>
<li><p><strong>State Transitions</strong>: Verify that the state correctly transitions through loading, data, and error states.</p>
</li>
<li><p><strong>Error Handling</strong>: Test error scenarios by simulating failures in your async operations.</p>
</li>
<li><p><strong>Timing</strong>: Use Future.delayed or similar methods to allow for asynchronous operations to complete.</p>
</li>
<li><p><strong>State Access</strong>: Access the current state using <a target="_blank" href="http://container.read">container.read</a>(provider) and the notifier using <a target="_blank" href="http://container.read">container.read</a>(provider.notifier).</p>
</li>
</ul>
<p>Testing AsyncNotifierProvider thoroughly ensures that your application can handle complex asynchronous workflows correctly, including proper error management and state transitions.</p>
<h3 id="heading-6-futureprovider">6. FutureProvider</h3>
<h4 id="heading-what-is-it-5">What is it?</h4>
<p>FutureProvider is used for asynchronous operations that don't need to be refreshed frequently.</p>
<h4 id="heading-when-to-use-it-5">When to use it?</h4>
<p>Use FutureProvider for:</p>
<ul>
<li><p>API calls that don't need real-time updates</p>
</li>
<li><p>Loading data that's fetched once and then cached</p>
</li>
</ul>
<h4 id="heading-testing-futureprovider">Testing FutureProvider</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> userProvider = FutureProvider&lt;User&gt;((ref) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>)); <span class="hljs-comment">// Simulating network delay</span>
  <span class="hljs-keyword">return</span> User(<span class="hljs-string">'John Doe'</span>);
});

<span class="hljs-keyword">void</span> main() {
  test(<span class="hljs-string">'userProvider loads data correctly'</span>, () <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> container = createContainer();

    expect(container.read(userProvider), <span class="hljs-keyword">const</span> AsyncValue&lt;User&gt;.loading());

    <span class="hljs-keyword">await</span> container.read(userProvider.future);

    <span class="hljs-keyword">final</span> value = container.read(userProvider);
    expect(value, isA&lt;AsyncData&lt;User&gt;&gt;());
    expect(value.value?.name, <span class="hljs-string">'John Doe'</span>);
  });
}
</code></pre>
<h4 id="heading-why-test-this-way-5">Why test this way?</h4>
<ul>
<li><p>We check the initial loading state.</p>
</li>
<li><p>We await the future to complete.</p>
</li>
<li><p>We verify the loaded data is correct and in the expected AsyncValue wrapper.</p>
</li>
</ul>
<h3 id="heading-7-streamprovider">7. StreamProvider</h3>
<h4 id="heading-what-is-it-6">What is it?</h4>
<p>StreamProvider is used for asynchronous data that changes over time.</p>
<h4 id="heading-when-to-use-it-6">When to use it?</h4>
<p>Use StreamProvider for:</p>
<ul>
<li><p>Real-time data updates (e.g., WebSocket connections)</p>
</li>
<li><p>Observing changes in databases or other data sources</p>
</li>
</ul>
<h4 id="heading-testing-streamprovider">Testing StreamProvider</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> countStreamProvider = StreamProvider&lt;<span class="hljs-built_in">int</span>&gt;((ref) {
  <span class="hljs-keyword">return</span> Stream.periodic(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>), (i) =&gt; i).take(<span class="hljs-number">3</span>);
});

<span class="hljs-keyword">void</span> main() {
  test(<span class="hljs-string">'countStreamProvider emits values correctly'</span>, () <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> container = createContainer();

    expect(container.read(countStreamProvider), <span class="hljs-keyword">const</span> AsyncValue&lt;<span class="hljs-built_in">int</span>&gt;.loading());

    <span class="hljs-keyword">await</span> expectLater(
      container.stream(countStreamProvider.future),
      emitsInOrder([<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>]),
    );

    <span class="hljs-keyword">final</span> finalValue = container.read(countStreamProvider);
    expect(finalValue, isA&lt;AsyncData&lt;<span class="hljs-built_in">int</span>&gt;&gt;());
    expect(finalValue.value, <span class="hljs-number">2</span>);
  });
}
</code></pre>
<h4 id="heading-why-test-this-way-6">Why test this way?</h4>
<ul>
<li><p>We check the initial loading state.</p>
</li>
<li><p>We use expectLater to verify the stream emits the expected sequence of values.</p>
</li>
<li><p>We check the final state to ensure it reflects the last emitted value.</p>
</li>
</ul>
<h3 id="heading-8-family-providers">8. Family Providers</h3>
<h4 id="heading-what-is-it-7">What is it?</h4>
<p>Family providers allow you to create providers that take parameters.</p>
<h4 id="heading-when-to-use-it-7">When to use it?</h4>
<p>Use Family providers when:</p>
<ul>
<li><p>You need to create multiple instances of a provider with different parameters</p>
</li>
<li><p>The provider depends on external data not available at compile-time</p>
</li>
</ul>
<h4 id="heading-testing-family-providers">Testing Family Providers</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> userFamilyProvider = FutureProvider.family&lt;User, <span class="hljs-built_in">String</span>&gt;((ref, userId) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>)); <span class="hljs-comment">// Simulating network delay</span>
  <span class="hljs-keyword">return</span> User(userId);
});

<span class="hljs-keyword">void</span> main() {
  test(<span class="hljs-string">'userFamilyProvider loads different users based on parameter'</span>, () <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> container = createContainer();

    <span class="hljs-keyword">final</span> user1Future = container.read(userFamilyProvider(<span class="hljs-string">'user1'</span>).future);
    <span class="hljs-keyword">final</span> user2Future = container.read(userFamilyProvider(<span class="hljs-string">'user2'</span>).future);

    <span class="hljs-keyword">final</span> user1 = <span class="hljs-keyword">await</span> user1Future;
    <span class="hljs-keyword">final</span> user2 = <span class="hljs-keyword">await</span> user2Future;

    expect(user1.id, <span class="hljs-string">'user1'</span>);
    expect(user2.id, <span class="hljs-string">'user2'</span>);
  });
}
</code></pre>
<h4 id="heading-why-test-this-way-7">Why test this way?</h4>
<ul>
<li><p>We test multiple instances of the family provider with different parameters.</p>
</li>
<li><p>We ensure that each instance returns the correct data based on its parameter.</p>
</li>
<li><p>We verify that family providers can be used concurrently without interference.</p>
</li>
</ul>
<h2 id="heading-advanced-testing-techniques">Advanced Testing Techniques</h2>
<h3 id="heading-using-overrides-for-mocking">Using Overrides for Mocking</h3>
<p>Overrides are particularly useful when you need to mock dependencies or test different scenarios. Here's an example:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> apiClientProvider = Provider&lt;ApiClient&gt;((ref) =&gt; RealApiClient());

<span class="hljs-keyword">final</span> userProvider = FutureProvider&lt;User&gt;((ref) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> client = ref.watch(apiClientProvider);
  <span class="hljs-keyword">return</span> client.fetchUser();
});

test(<span class="hljs-string">'userProvider fetches user correctly'</span>, () <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> mockClient = MockApiClient();
  when(mockClient.fetchUser()).thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; User(<span class="hljs-string">'Test User'</span>));

  <span class="hljs-keyword">final</span> container = createContainer(
    overrides: [
      apiClientProvider.overrideWithValue(mockClient),
    ],
  );

  <span class="hljs-keyword">final</span> user = <span class="hljs-keyword">await</span> container.read(userProvider.future);
  expect(user.name, <span class="hljs-string">'Test User'</span>);
});
</code></pre>
<p>In this example, we're overriding the apiClientProvider with a mock implementation, allowing us to test the userProvider in isolation without making real API calls.</p>
<h2 id="heading-widget-testing-with-riverpod">Widget Testing with Riverpod</h2>
<p>Widget testing is crucial for ensuring that your UI components behave correctly when integrated with Riverpod providers. Let's explore how to effectively test widgets that use Riverpod, utilizing some helpful extensions from your testing utilities.</p>
<h3 id="heading-widgettester-extensions">WidgetTester Extensions</h3>
<p>First, let's look at the useful extensions you've defined for WidgetTester:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">extension</span> WidgetTesterX <span class="hljs-keyword">on</span> WidgetTester {
  <span class="hljs-comment">/// <span class="markdown">Calls [pumpWidget] with the [widget] wrapped in a [MaterialApp].</span></span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; pumpMaterialWidget(
    Widget widget, [
    <span class="hljs-built_in">Duration?</span> duration,
    EnginePhase phase = EnginePhase.sendSemanticsUpdate,
  ]) =&gt;
      pumpWidget(MaterialApp(home: widget));

  <span class="hljs-comment">/// <span class="markdown">Calls [pumpWidget] with the [widget] wrapped in a [MaterialApp] and scoped</span></span>
  <span class="hljs-comment">/// <span class="markdown">with a [ProviderScope].</span></span>
  <span class="hljs-comment">///
  <span class="markdown">/// The [overrides] and [observers] values will be passed to</span></span>
  <span class="hljs-comment">/// <span class="markdown">the [ProviderScope].</span></span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; pumpMaterialWidgetScoped(
    Widget widget, {
    <span class="hljs-built_in">Duration?</span> duration,
    EnginePhase phase = EnginePhase.sendSemanticsUpdate,
    <span class="hljs-built_in">List</span>&lt;Override&gt; overrides = <span class="hljs-keyword">const</span> [],
    <span class="hljs-built_in">List</span>&lt;ProviderObserver&gt;? observers,
  }) =&gt;
      pumpWidget(
        ProviderScope(
          overrides: overrides,
          observers: observers,
          child: MaterialApp(home: widget),
        ),
      );
}
</code></pre>
<p>These extensions provide convenient methods for pumping widgets wrapped in a MaterialApp and ProviderScope, which is essential for testing Riverpod-dependent widgets.</p>
<h3 id="heading-testing-a-widget-with-riverpod">Testing a Widget with Riverpod</h3>
<p>Let's create an example widget that uses a Riverpod provider and then test it:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// counter_widget.dart</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:hooks_riverpod/hooks_riverpod.dart'</span>;

<span class="hljs-keyword">final</span> counterProvider = StateProvider((ref) =&gt; <span class="hljs-number">0</span>);

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CounterWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ConsumerWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context, WidgetRef ref) {
    <span class="hljs-keyword">final</span> count = ref.watch(counterProvider);
    <span class="hljs-keyword">return</span> Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(<span class="hljs-string">'Count: <span class="hljs-subst">$count</span>'</span>),
        ElevatedButton(
          onPressed: () =&gt; ref.read(counterProvider.notifier).state++,
          child: Text(<span class="hljs-string">'Increment'</span>),
        ),
      ],
    );
  }
}
</code></pre>
<p>Now, let's write a test for this widget:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// counter_widget_test.dart</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_test/flutter_test.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:hooks_riverpod/hooks_riverpod.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:your_app/counter_widget.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:your_app/test/utils/testing_utils.dart'</span>;

<span class="hljs-keyword">void</span> main() {
  testWidgets(<span class="hljs-string">'CounterWidget displays count and increments'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> tester.pumpMaterialWidgetScoped(CounterWidget());

    <span class="hljs-comment">// Check initial state</span>
    expect(find.text(<span class="hljs-string">'Count: 0'</span>), findsOneWidget);

    <span class="hljs-comment">// Tap the increment button</span>
    <span class="hljs-keyword">await</span> tester.tap(find.byType(ElevatedButton));
    <span class="hljs-keyword">await</span> tester.pump();

    <span class="hljs-comment">// Check updated state</span>
    expect(find.text(<span class="hljs-string">'Count: 1'</span>), findsOneWidget);
  });

  testWidgets(<span class="hljs-string">'CounterWidget with initial value override'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> tester.pumpMaterialWidgetScoped(
      CounterWidget(),
      overrides: [
        counterProvider.overrideWith((ref) =&gt; <span class="hljs-number">10</span>),
      ],
    );

    <span class="hljs-comment">// Check initial state with override</span>
    expect(find.text(<span class="hljs-string">'Count: 10'</span>), findsOneWidget);
  });
}
</code></pre>
<h3 id="heading-key-points-for-widget-testing-with-riverpod">Key Points for Widget Testing with Riverpod</h3>
<p>1. <strong>Use pumpMaterialWidgetScoped</strong>: This extension method wraps your widget in both a MaterialApp and a ProviderScope, which is necessary for most Riverpod-based widgets.</p>
<ul>
<li><p><strong>Overrides for Testing</strong>: You can pass overrides to pumpMaterialWidgetScoped to replace providers with test-specific values or mocks.</p>
</li>
<li><p><strong>Interacting with Widgets</strong>: Use tester.tap(), tester.enterText(), etc., to interact with your widgets, then use tester.pump() or tester.pumpAndSettle() to rebuild the widget tree.</p>
</li>
<li><p><strong>Verifying UI Updates</strong>: After interactions, check that the UI has updated correctly using expect() and finder methods.</p>
</li>
<li><p><strong>Testing Different Scenarios</strong>: Create multiple test cases to cover different initial states, user interactions, and edge cases.</p>
</li>
</ul>
<h3 id="heading-testing-asynchronous-widgets">Testing Asynchronous Widgets</h3>
<p>For widgets that depend on asynchronous providers (like FutureProvider or StreamProvider), you may need to use tester.pumpAndSettle() or manually pump with a duration to allow for asynchronous operations to complete:</p>
<pre><code class="lang-dart">testWidgets(<span class="hljs-string">'AsyncCounterWidget displays loading and then value'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> asyncCounterProvider = FutureProvider((ref) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>));
    <span class="hljs-keyword">return</span> <span class="hljs-number">42</span>;
  });

  <span class="hljs-keyword">await</span> tester.pumpMaterialWidgetScoped(
    Consumer(builder: (context, ref, _) {
      <span class="hljs-keyword">final</span> asyncValue = ref.watch(asyncCounterProvider);
      <span class="hljs-keyword">return</span> asyncValue.when(
        data: (value) =&gt; Text(<span class="hljs-string">'Count: <span class="hljs-subst">$value</span>'</span>),
        loading: () =&gt; CircularProgressIndicator(),
        error: (_, __) =&gt; Text(<span class="hljs-string">'Error'</span>),
      );
    }),
  );

  <span class="hljs-comment">// Initially, we should see a loading indicator</span>
  expect(find.byType(CircularProgressIndicator), findsOneWidget);

  <span class="hljs-comment">// Wait for the future to complete</span>
  <span class="hljs-keyword">await</span> tester.pumpAndSettle(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">2</span>));

  <span class="hljs-comment">// Now we should see the value</span>
  expect(find.text(<span class="hljs-string">'Count: 42'</span>), findsOneWidget);
});
</code></pre>
<p>By incorporating these widget testing techniques with Riverpod, you can ensure that your UI components interact correctly with your state management logic, leading to more robust and reliable Flutter applications.</p>
<h2 id="heading-best-practices-for-riverpod-testing">Best Practices for Riverpod Testing</h2>
<ol>
<li><p><strong>Isolation</strong>: Always use createContainer to isolate providers during testing.</p>
</li>
<li><p><strong>Disposal</strong>: The createContainer utility automatically handles disposal, but ensure you're not keeping references that prevent garbage collection.</p>
</li>
<li><p><strong>Async Handling</strong>: For async providers, use expectLater or await appropriately.</p>
</li>
<li><p><strong>State Changes</strong>: Test both initial states and state transitions.</p>
</li>
<li><p><strong>Mocking and Overrides</strong>: Use overrides in createContainer to mock dependencies or provide test-specific implementations. This allows you to isolate the component under test and control its environment.</p>
</li>
<li><p><strong>Comprehensive Coverage</strong>: Test edge cases and error scenarios, not just the happy path.</p>
</li>
<li><p><strong>Readability</strong>: Structure tests in a "Arrange-Act-Assert" pattern for clarity.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By understanding Riverpod's core concepts, including ProviderContainer, overrides, and the various provider types, you can create a comprehensive and robust test suite. The createContainer utility and judicious use of overrides allow you to precisely control the testing environment, focusing on specific behaviors without unnecessary dependencies.</p>
<p>This approach leads to more reliable, maintainable, and performant Flutter applications. As Riverpod continues to evolve, always refer to the official documentation for the most up-to-date information and advanced usage patterns.</p>
]]></content:encoded></item><item><title><![CDATA[Taming the Beast: Mastering Forms in Flutter]]></title><description><![CDATA[Ever found yourself pulling your hair out over form handling in your apps?
Trust me, I've been there. Forms can be a real pain, but they're also the bread and butter of most apps. So, let's roll up our sleeves and dive into the world of Flutter forms...]]></description><link>https://article.temiajiboye.com/mastering-forms-in-flutter</link><guid isPermaLink="true">https://article.temiajiboye.com/mastering-forms-in-flutter</guid><dc:creator><![CDATA[Temitope Ajiboye]]></dc:creator><pubDate>Wed, 21 Aug 2024 00:55:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724201247100/e6fb0e71-abc2-49db-8bdb-e71749cdc89c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ever found yourself pulling your hair out over form handling in your apps?</p>
<p>Trust me, I've been there. Forms can be a real pain, but they're also the bread and butter of most apps. So, let's roll up our sleeves and dive into the world of Flutter forms together. By the end of this, you'll be creating forms so smooth, users might actually enjoy filling them out.</p>
<h2 id="heading-setting-the-stage">Setting the Stage</h2>
<p>First things first, we need to invite some friends to our form-building party.</p>
<p>Open up your <strong>pubspec.yaml</strong> file and add these cool cats:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">dependencies:</span>
  <span class="hljs-attr">flutter:</span>
    <span class="hljs-attr">sdk:</span> <span class="hljs-string">flutter</span>
  <span class="hljs-attr">flutter_hooks:</span> <span class="hljs-string">^0.20.5</span>
  <span class="hljs-attr">hooks_riverpod:</span> <span class="hljs-string">^2.5.2</span>
  <span class="hljs-attr">riverpod_annotation:</span> <span class="hljs-string">^2.3.5</span>
  <span class="hljs-attr">freezed_annotation:</span> <span class="hljs-string">^2.4.4</span> 
  <span class="hljs-attr">json_annotation:</span> <span class="hljs-string">^4.9.0</span>

<span class="hljs-attr">dev_dependencies:</span>
  <span class="hljs-attr">flutter_test:</span>
    <span class="hljs-attr">sdk:</span> <span class="hljs-string">flutter</span>
  <span class="hljs-attr">build_runner:</span> <span class="hljs-string">^2.4.9</span>
  <span class="hljs-attr">riverpod_generator:</span> <span class="hljs-string">^2.4.0</span>
  <span class="hljs-attr">freezed:</span> <span class="hljs-string">^2.5.2</span>
  <span class="hljs-attr">json_serializable:</span> <span class="hljs-string">^6.8.0</span>
</code></pre>
<p>Now, run <code>flutter pub get</code> to fetch these packages. It's like ordering pizza for your project – essential for a good time!</p>
<p>But why these specific packages, you ask?</p>
<p>Well, <strong>flutter_hooks</strong> is like a Swiss Army knife for state management in widgets. <strong>hooks_riverpod</strong> combines the power of Riverpod (our state management superhero) with Flutter Hooks. And <strong>riverpod_annotation</strong> with <strong>riverpod_generator</strong>? They're the dynamic duo that'll help us write less boilerplate code.</p>
<h2 id="heading-the-vip-userprofile-class">The VIP: UserProfile Class</h2>
<p>Let's create our star player, the UserProfile class. Think of it as the VIP of our app – it's got all the important info:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserProfile</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> username;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> email;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int</span> age;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> notificationsEnabled;

  UserProfile({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.username,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.email,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.age,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.notificationsEnabled,
  });

  UserProfile copyWith({
    <span class="hljs-built_in">String?</span> username,
    <span class="hljs-built_in">String?</span> email,
    <span class="hljs-built_in">int?</span> age,
    <span class="hljs-built_in">bool?</span> notificationsEnabled,
  }) {
    <span class="hljs-keyword">return</span> UserProfile(
      username: username ?? <span class="hljs-keyword">this</span>.username,
      email: email ?? <span class="hljs-keyword">this</span>.email,
      age: age ?? <span class="hljs-keyword">this</span>.age,
      notificationsEnabled: notificationsEnabled ?? <span class="hljs-keyword">this</span>.notificationsEnabled,
    );
  }
}
</code></pre>
<p>See that copyWith method? It's like a clone machine for our UserProfile. Super handy when we want to update just one or two fields!</p>
<p>Do we really need to write this by hand?</p>
<p>Now, let's rewrite our UserProfile class using <strong>Freezed</strong>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:freezed_annotation/freezed_annotation.dart'</span>;

<span class="hljs-keyword">part</span> <span class="hljs-string">'user_profile.freezed.dart'</span>;
<span class="hljs-keyword">part</span> <span class="hljs-string">'user_profile.g.dart'</span>;

<span class="hljs-meta">@freezed</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserProfile</span> <span class="hljs-title">with</span> <span class="hljs-title">_</span>$<span class="hljs-title">UserProfile</span> </span>{
  <span class="hljs-keyword">const</span> <span class="hljs-keyword">factory</span> UserProfile({
    <span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> username,
    <span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> email,
    <span class="hljs-keyword">required</span> <span class="hljs-built_in">int</span> age,
    <span class="hljs-meta">@Default</span>(<span class="hljs-keyword">true</span>) <span class="hljs-built_in">bool</span> notificationsEnabled,
  }) = _UserProfile;

  <span class="hljs-keyword">factory</span> UserProfile.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) =&gt; _$UserProfileFromJson(json);
}
</code></pre>
<p>Whoa, what just happened? We've turned our UserProfile into a Freezed class. It's like giving it superpowers:</p>
<ol>
<li><p>Immutability: This class is now as unchangeable as your grandma's secret recipe.</p>
</li>
<li><p>Auto-generated copyWith: No more hand-writing clone methods!</p>
</li>
<li><p>Equality comparisons: Two profiles with the same data are now considered equal. Identity crisis averted!</p>
</li>
<li><p>JSON superpowers: Serialization and deserialization come built-in. API integration just got a whole lot easier!</p>
</li>
<li><p>Pattern matching: For when you want to get fancy with your data handling.</p>
</li>
</ol>
<h2 id="heading-the-personal-assistant-userprofilenotifier">The Personal Assistant: UserProfileNotifier</h2>
<p>Now, we need someone to manage our VIP. Enter the UserProfileNotifier:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:riverpod_annotation/riverpod_annotation.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../models/user_profile.dart'</span>;

<span class="hljs-keyword">part</span> <span class="hljs-string">'user_profile_provider.g.dart'</span>;
<span class="hljs-meta">@riverpod</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserProfileNotifier</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">_</span>$<span class="hljs-title">UserProfileNotifier</span> </span>{
  <span class="hljs-meta">@override</span>
  UserProfile build() {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">const</span> UserProfile(
      username: <span class="hljs-string">'JohnDoe'</span>,
      email: <span class="hljs-string">'john@example.com'</span>,
      age: <span class="hljs-number">30</span>,
      notificationsEnabled: <span class="hljs-keyword">true</span>,
    );
  }

  <span class="hljs-keyword">void</span> updateProfile({
    <span class="hljs-built_in">String?</span> username,
    <span class="hljs-built_in">String?</span> email,
    <span class="hljs-built_in">int?</span> age,
    <span class="hljs-built_in">bool?</span> notificationsEnabled,
  }) {
    state = state.copyWith(
      username: username ?? state.username,
      email: email ?? state.email,
      age: age ?? state.age,
      notificationsEnabled: notificationsEnabled ?? state.notificationsEnabled,
    );
  }
}
</code></pre>
<p>This notifier is like a personal assistant for our UserProfile. It keeps track of changes and updates the profile when needed. The @riverpod annotation is telling our code generator, "Hey, make some cool stuff for this class!"</p>
<p>After creating this file, don't forget to run:</p>
<p>It's like telling your assistant, "Hey, organize all this stuff for me!"</p>
<p><code>flutter pub run build_runner build --delete-conflicting-outputs</code></p>
<h2 id="heading-the-main-event-profilesettingsform">The Main Event: ProfileSettingsForm</h2>
<p>Alright, now for the main event – the form itself. We'll create a ProfileSettingsForm widget. This is where users can tweak their profile info:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_hooks/flutter_hooks.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:hooks_riverpod/hooks_riverpod.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../providers/user_profile_provider.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileSettingsForm</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HookConsumerWidget</span> </span>{
  <span class="hljs-keyword">const</span> ProfileSettingsForm({Key? key}) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context, WidgetRef ref) {
    <span class="hljs-keyword">final</span> userProfile = ref.watch(userProfileNotifierProvider);
    <span class="hljs-keyword">final</span> notifier = ref.read(userProfileNotifierProvider.notifier);

    <span class="hljs-keyword">final</span> usernameController = useTextEditingController(text: userProfile.username);
    <span class="hljs-keyword">final</span> emailController = useTextEditingController(text: userProfile.email);
    <span class="hljs-keyword">final</span> ageController = useTextEditingController(text: userProfile.age.toString());
    <span class="hljs-keyword">final</span> notificationsEnabled = useState(userProfile.notificationsEnabled);

    <span class="hljs-comment">// We'll add more code here soon!</span>

    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: Text(<span class="hljs-string">'Profile Settings'</span>)),
      body: Padding(
        padding: EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Column(
          children: [
            TextFormField(
              controller: usernameController,
              decoration: InputDecoration(labelText: <span class="hljs-string">'Username'</span>),
            ),
            TextFormField(
              controller: emailController,
              decoration: InputDecoration(labelText: <span class="hljs-string">'Email'</span>),
            ),
            TextFormField(
              controller: ageController,
              decoration: InputDecoration(labelText: <span class="hljs-string">'Age'</span>),
              keyboardType: TextInputType.number,
            ),
            SwitchListTile(
              title: Text(<span class="hljs-string">'Enable Notifications'</span>),
              value: notificationsEnabled.value,
              onChanged: (value) =&gt; notificationsEnabled.value = value,
            ),
            <span class="hljs-comment">// We'll add a save button here soon!</span>
          ],
        ),
      ),
    );
  }
}
</code></pre>
<p>Notice how we're using copyWith in the saveProfile method? That's our Freezed magic making profile updates a breeze!</p>
<h2 id="heading-the-freezed-cherry-on-top">The Freezed Cherry on Top</h2>
<p>After making all these cool changes, don't forget to tell your project to generate all the magical code:</p>
<p><code>flutter pub run build_runner build --delete-conflicting-outputs</code></p>
<p>This form is like a backstage pass to the user's profile. We're using <strong>HookConsumerWidget</strong> because it's the cool kid that lets us use both Riverpod and Hooks in the same widget.</p>
<h2 id="heading-the-watchful-eye-tracking-form-changes">The Watchful Eye: Tracking Form Changes</h2>
<p>We want to know when the user makes changes, right? Let's set up a little spy system:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> isFormChanged = useState(<span class="hljs-keyword">false</span>);

useEffect(() {
  <span class="hljs-keyword">void</span> listener() {
    isFormChanged.value = usernameController.text != userProfile.username ||
        emailController.text != userProfile.email ||
        ageController.text != userProfile.age.toString() ||
        notificationsEnabled.value != userProfile.notificationsEnabled;
  }

  usernameController.addListener(listener);
  emailController.addListener(listener);
  ageController.addListener(listener);

  <span class="hljs-keyword">return</span> () {
    usernameController.removeListener(listener);
    emailController.removeListener(listener);
    ageController.removeListener(listener);
  };
}, [userProfile]);
</code></pre>
<p>This useEffect hook is like a vigilant guard, always watching for changes in the form. It's comparing the current values to the original ones, and if anything's different, it raises the <em>isFormChanged</em> flag.</p>
<h2 id="heading-the-bouncer-input-validation">The Bouncer: Input Validation</h2>
<p>We can't just let any old data through, can we? Time for some validation:</p>
<pre><code class="lang-dart"><span class="hljs-built_in">bool</span> validateInputs() {
  <span class="hljs-keyword">final</span> isValidEmail = <span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'</span>).hasMatch(emailController.text);
  <span class="hljs-keyword">final</span> isValidAge = <span class="hljs-built_in">int</span>.tryParse(ageController.text) != <span class="hljs-keyword">null</span> &amp;&amp; <span class="hljs-built_in">int</span>.parse(ageController.text) &gt; <span class="hljs-number">0</span>;
  <span class="hljs-keyword">return</span> usernameController.text.isNotEmpty &amp;&amp; isValidEmail &amp;&amp; isValidAge;
}
</code></pre>
<p>This function is like the bouncer at an exclusive club. It's checking if the email looks legit, the age is a positive number, and the username isn't empty. Only the cool kids (valid inputs) get through!</p>
<h2 id="heading-the-grand-finale-saving-the-profile">The Grand Finale: Saving the Profile</h2>
<p>When the user hits that save button, we want to make sure everything's shipshape:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> saveProfile() {
  <span class="hljs-keyword">if</span> (!validateInputs()) <span class="hljs-keyword">return</span>;

  notifier.updateProfile(
    username: usernameController.text,
    email: emailController.text,
    age: <span class="hljs-built_in">int</span>.parse(ageController.text),
    notificationsEnabled: notificationsEnabled.value,
  );

  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(<span class="hljs-string">'Profile updated successfully'</span>)),
  );

  isFormChanged.value = <span class="hljs-keyword">false</span>;
}
</code></pre>
<p>This function is like a careful librarian, making sure all the info is filed away correctly. If everything checks out, it updates the profile, shows a snazzy success message, and resets our change tracker.</p>
<p>Now, let's add that save button to our form:</p>
<pre><code class="lang-dart">ElevatedButton(
  onPressed: isFormChanged.value &amp;&amp; validateInputs() ? saveProfile : <span class="hljs-keyword">null</span>,
  child: Text(<span class="hljs-string">'Save Profile'</span>),
)
</code></pre>
<p>This button is smart - it only lights up when there are changes and all inputs are valid. No premature saving here!</p>
<h2 id="heading-everything-together">Everything Together</h2>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_hooks/flutter_hooks.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:hooks_riverpod/hooks_riverpod.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../providers/user_profile_provider.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileSettingsForm</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HookConsumerWidget</span> </span>{
  <span class="hljs-keyword">const</span> ProfileSettingsForm({Key? key}) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context, WidgetRef ref) {
    <span class="hljs-keyword">final</span> userProfile = ref.watch(userProfileNotifierProvider);
    <span class="hljs-keyword">final</span> notifier = ref.read(userProfileNotifierProvider.notifier);

    <span class="hljs-keyword">final</span> usernameController = useTextEditingController(text: userProfile.username);
    <span class="hljs-keyword">final</span> emailController = useTextEditingController(text: userProfile.email);
    <span class="hljs-keyword">final</span> ageController = useTextEditingController(text: userProfile.age.toString());
    <span class="hljs-keyword">final</span> notificationsEnabled = useState(userProfile.notificationsEnabled);

    <span class="hljs-keyword">final</span> isFormChanged = useState(<span class="hljs-keyword">false</span>);

    useEffect(() {
      <span class="hljs-keyword">void</span> listener() {
        isFormChanged.value = usernameController.text != userProfile.username ||
            emailController.text != userProfile.email ||
            ageController.text != userProfile.age.toString() ||
            notificationsEnabled.value != userProfile.notificationsEnabled;
      }

      usernameController.addListener(listener);
      emailController.addListener(listener);
      ageController.addListener(listener);

      <span class="hljs-keyword">return</span> () {
        usernameController.removeListener(listener);
        emailController.removeListener(listener);
        ageController.removeListener(listener);
      };
    }, [userProfile]);

    <span class="hljs-built_in">bool</span> validateInputs() {
      <span class="hljs-keyword">final</span> isValidEmail = <span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'</span>).hasMatch(emailController.text);
      <span class="hljs-keyword">final</span> isValidAge = <span class="hljs-built_in">int</span>.tryParse(ageController.text) != <span class="hljs-keyword">null</span> &amp;&amp; <span class="hljs-built_in">int</span>.parse(ageController.text) &gt; <span class="hljs-number">0</span>;
      <span class="hljs-keyword">return</span> usernameController.text.isNotEmpty &amp;&amp; isValidEmail &amp;&amp; isValidAge;
    }

    <span class="hljs-keyword">void</span> saveProfile() {
      <span class="hljs-keyword">if</span> (!validateInputs()) <span class="hljs-keyword">return</span>;

      <span class="hljs-keyword">final</span> updatedProfile = userProfile.copyWith(
        username: usernameController.text,
        email: emailController.text,
        age: <span class="hljs-built_in">int</span>.parse(ageController.text),
        notificationsEnabled: notificationsEnabled.value,
      );

      notifier.updateProfile(
        username: updatedProfile.username,
        email: updatedProfile.email,
        age: updatedProfile.age,
        notificationsEnabled: updatedProfile.notificationsEnabled,
      );

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(<span class="hljs-string">'Profile updated successfully'</span>)),
      );

      isFormChanged.value = <span class="hljs-keyword">false</span>;
    }

    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: Text(<span class="hljs-string">'Profile Settings'</span>)),
      body: Padding(
        padding: EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Column(
          children: [
            TextFormField(
              controller: usernameController,
              decoration: InputDecoration(labelText: <span class="hljs-string">'Username'</span>),
            ),
            TextFormField(
              controller: emailController,
              decoration: InputDecoration(labelText: <span class="hljs-string">'Email'</span>),
            ),
            TextFormField(
              controller: ageController,
              decoration: InputDecoration(labelText: <span class="hljs-string">'Age'</span>),
              keyboardType: TextInputType.number,
            ),
            SwitchListTile(
              title: Text(<span class="hljs-string">'Enable Notifications'</span>),
              value: notificationsEnabled.value,
              onChanged: (value) =&gt; notificationsEnabled.value = value,
            ),
            ElevatedButton(
              onPressed: isFormChanged.value &amp;&amp; validateInputs() ? saveProfile : <span class="hljs-keyword">null</span>,
              child: Text(<span class="hljs-string">'Save Profile'</span>),
            ),
          ],
        ),
      ),
    );
  }
}
</code></pre>
<h2 id="heading-pro-tips-for-form-mastery">Pro Tips for Form Mastery</h2>
<ol>
<li><p><strong>Debounce like a Pro</strong>: If you're doing something heavy (like API calls) on every keystroke, use debouncing. It's like giving your app a chill pill:</p>
<pre><code class="lang-dart">    <span class="hljs-keyword">final</span> debouncedUsername = useDebounce(usernameController.text, <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">300</span>));

    useEffect(() {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Checking availability for username: <span class="hljs-subst">$debouncedUsername</span>'</span>);
      <span class="hljs-comment">// Perform API call here</span>
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    }, [debouncedUsername]);
</code></pre>
</li>
<li><p><strong>Error Messages with Attitude</strong>: Don't just say "Invalid input". Get creative!</p>
<pre><code class="lang-dart">    TextFormField(
      controller: emailController,
      decoration: InputDecoration(
        labelText: <span class="hljs-string">'Email'</span>,
        errorText: !<span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'</span>).hasMatch(emailController.text)
            ? <span class="hljs-string">'That email looks about as real as a three-dollar bill'</span>
            : <span class="hljs-keyword">null</span>,
      ),
    )
</code></pre>
</li>
<li><p><strong>Accessibility is Key</strong>: Make your forms accessible. It's not just nice, it's necessary:</p>
<pre><code class="lang-dart">    TextFormField(
      controller: usernameController,
      decoration: InputDecoration(labelText: <span class="hljs-string">'Username'</span>),
      autofocus: <span class="hljs-keyword">true</span>, <span class="hljs-comment">// Focus on this field when the form loads</span>
      textInputAction: TextInputAction.next, <span class="hljs-comment">// Move to next field on submit</span>
    )
</code></pre>
</li>
<li><p><strong>Form Keys</strong>: For more complex forms, use a GlobalKey&lt;FormState&gt;:</p>
<pre><code class="lang-dart"> <span class="hljs-keyword">final</span> formKey = GlobalKey&lt;FormState&gt;();

 <span class="hljs-comment">// In your build method:</span>
 Form(
   key: formKey,
   child: Column(
     children: [
       <span class="hljs-comment">// Your form fields here</span>
     ],
   ),
 )

 <span class="hljs-comment">// Validate the entire form:</span>
 <span class="hljs-keyword">if</span> (formKey.currentState!.validate()) {
   <span class="hljs-comment">// Form is valid, proceed with saving</span>
 }
</code></pre>
</li>
<li><p><strong>Separation of Concerns</strong>: Use Riverpod providers to handle business logic:</p>
<pre><code class="lang-dart"> <span class="hljs-meta">@riverpod</span>
 Future&lt;<span class="hljs-built_in">bool</span>&gt; checkUsernameAvailability(CheckUsernameAvailabilityRef ref, <span class="hljs-built_in">String</span> username) <span class="hljs-keyword">async</span> {
   <span class="hljs-comment">// Implement API call to check username availability</span>
   <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>)); <span class="hljs-comment">// Simulating API call</span>
   <span class="hljs-keyword">return</span> username != <span class="hljs-string">'takenUsername'</span>;
 }
</code></pre>
</li>
<li><p><strong>Test, Test, Test</strong>: Write tests for your forms. It's like proofreading your work – boring but essential:</p>
<pre><code class="lang-dart">    testWidgets(<span class="hljs-string">'ProfileSettingsForm shows validation errors'</span>, (WidgetTester tester) <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">await</span> tester.pumpWidget(ProviderScope(child: MaterialApp(home: ProfileSettingsForm())));

      <span class="hljs-keyword">await</span> tester.enterText(find.byType(TextFormField).at(<span class="hljs-number">1</span>), <span class="hljs-string">'invalid-email'</span>);
      <span class="hljs-keyword">await</span> tester.pump();

      expect(find.text(<span class="hljs-string">'That email looks about as real as a three-dollar bill'</span>), findsOneWidget);
    });
</code></pre>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p> And there you have it, folks! We've taken the scary beast that is form handling in Flutter and tamed it into a purring kitten. With Riverpod for state management and hooks for local widget state, you've got a powerful combo that'll make your forms sing.</p>
<p> Remember, practice makes perfect. The more forms you build, the easier it gets. So go forth and create forms that are so good, users might actually enjoy filling them out. (Okay, maybe that's a stretch, but we can dream, right?)</p>
<p> Happy coding, and may your forms always validate on the first try!</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Testing Permission Handling in Flutter: A Comprehensive Guide]]></title><description><![CDATA[When developing Flutter applications that require device permissions, it's crucial to thoroughly test your permission-handling logic.
This article will guide you through the process of testing a permission service class, using a PermissionHandlerAppP...]]></description><link>https://article.temiajiboye.com/testing-permission-handling-in-flutter-a-comprehensive-guide</link><guid isPermaLink="true">https://article.temiajiboye.com/testing-permission-handling-in-flutter-a-comprehensive-guide</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Flutter Widgets]]></category><dc:creator><![CDATA[Temitope Ajiboye]]></dc:creator><pubDate>Sun, 18 Aug 2024 10:55:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1723978274938/e2cf6c29-b3bb-425e-96c4-63c5dab894c1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When developing Flutter applications that require device permissions, it's crucial to thoroughly test your permission-handling logic.</p>
<p>This article will guide you through the process of testing a permission service class, using a <strong>PermissionHandlerAppPermissionsService</strong> as an example.</p>
<h2 id="heading-understanding-the-permission-service"><strong>Understanding the Permission Service</strong></h2>
<p>First, let's look at a simplified version of our PermissionHandlerAppPermissionsService:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:permission_handler/permission_handler.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../domain/app_permissions_service.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../domain/model/permission_type.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../domain/model/permission_status.dart'</span> <span class="hljs-keyword">as</span> domain;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PermissionHandlerAppPermissionsService</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AppPermissionsService</span> </span>{
  <span class="hljs-meta">@override</span>
  Future&lt;Result&lt;AppException&lt;PermissionErrorCode&gt;, domain.PermissionStatus&gt;&gt;
      requestPermission(PermissionType type) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> permission = type.toPermission();
      <span class="hljs-keyword">final</span> status = <span class="hljs-keyword">await</span> permission.request();
      <span class="hljs-keyword">return</span> Success(status.toDomain());
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-keyword">return</span> Failure(AppException(PermissionErrorCode.unknown));
    }
  }
}
</code></pre>
<p>This service uses the <a target="_blank" href="https://pub.dev/packages/permission_handler"><strong>permission_handler</strong></a> package to request permissions and handles the results.</p>
<h2 id="heading-setting-up-the-test-environment"><strong>Setting Up the Test Environment</strong></h2>
<p>To test this service effectively, we'll use a custom MethodChannelMock class.</p>
<p>This class allows us to simulate method channel calls without relying on the actual platform implementation. Here's the implementation:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/services.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_test/flutter_test.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MethodChannelMock</span> </span>{
  MethodChannelMock({
    <span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> channelName,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.method,
    <span class="hljs-keyword">this</span>.result,
    <span class="hljs-keyword">this</span>.delay = <span class="hljs-built_in">Duration</span>.zero,
  }) : methodChannel = MethodChannel(channelName) {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(methodChannel, _handler);
  }
  <span class="hljs-keyword">final</span> MethodChannel methodChannel;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> method;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">dynamic</span> result;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Duration</span> delay;

  Future _handler(MethodCall methodCall) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (methodCall.method != method) {
      <span class="hljs-keyword">throw</span> MissingPluginException(<span class="hljs-string">'No implementation found for method '</span>
          <span class="hljs-string">'<span class="hljs-subst">$method</span> on channel <span class="hljs-subst">${methodChannel.name}</span>'</span>);
    }

    <span class="hljs-keyword">return</span> Future.delayed(delay, () {
      <span class="hljs-keyword">if</span> (result <span class="hljs-keyword">is</span> Exception) {
        <span class="hljs-keyword">throw</span> result;
      }

      <span class="hljs-keyword">return</span> Future.value(result);
    });
  }
}
</code></pre>
<p>This custom mock class allows us to easily set up method channel mocks with specific results and delays.</p>
<p>This custom mock class is designed to simulate method channel calls in Flutter tests. Here's a breakdown of its key components:</p>
<ol>
<li><p><strong>Constructor</strong>:</p>
<ol>
<li><p>Takes channelName, method, result, and an optional delay.</p>
</li>
<li><p>Creates a MethodChannel with the given channelName.</p>
</li>
<li><p>Sets up a mock method call handler using TestDefaultBinaryMessengerBinding.</p>
</li>
</ol>
</li>
<li><p><strong>Properties</strong>:</p>
<ol>
<li><p>methodChannel: The MethodChannel being mocked.</p>
</li>
<li><p>method: The specific method name to mock.</p>
</li>
<li><p>result: The result to return when the method is called.</p>
</li>
<li><p>delay: An optional delay before returning the result.</p>
</li>
</ol>
</li>
<li><p><strong>_handler method</strong>:</p>
<ol>
<li><p>This is the core of the mock. It's called when the mocked method is invoked.</p>
</li>
<li><p>It checks if the called method matches the expected method name.</p>
</li>
<li><p>If there's a mismatch, it throws a MissingPluginException.</p>
</li>
<li><p>If the method matches, it returns the result after the specified delay.</p>
</li>
<li><p>If the result is an Exception, it throws the exception instead.</p>
</li>
</ol>
</li>
</ol>
<p>The class provides a flexible way to mock method channel calls by:</p>
<ul>
<li><p>Allowing specification of the exact method to mock.</p>
</li>
<li><p>Providing custom results or exceptions.</p>
</li>
<li><p>Simulating delays in responses.</p>
</li>
</ul>
<p>This approach allows for precise control over the behavior of platform channel interactions in tests, enabling thorough testing of various scenarios without relying on actual platform implementations.</p>
<h2 id="heading-writing-the-tests"><strong>Writing the Tests</strong></h2>
<p>Now, let's write some tests for our PermissionHandlerAppPermissionsService using the MethodChannelMock:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_test/flutter_test.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/services.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:your_app/modules/permissions/data/permission_handler_app_permissions_service.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:your_app/modules/permissions/domain/model/permission_type.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:your_app/modules/permissions/domain/model/permission_status.dart'</span> <span class="hljs-keyword">as</span> domain;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../../common/method_channel_mock.dart'</span>;

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">late</span> PermissionHandlerAppPermissionsService service;
  <span class="hljs-keyword">late</span> MethodChannelMock methodChannelMock;

  setUp(() {
    service = PermissionHandlerAppPermissionsService();
    methodChannelMock = MethodChannelMock(
      channelName: <span class="hljs-string">'flutter.baseflow.com/permissions/methods'</span>,
      method: <span class="hljs-string">'requestPermissions'</span>,
      result: {<span class="hljs-number">0</span>: <span class="hljs-number">1</span>}, <span class="hljs-comment">// This represents PermissionStatus.granted</span>
    );
  });

  test(<span class="hljs-string">'requestPermission returns Success with granted status'</span>, () <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> service.requestPermission(PermissionType.camera);

    expect(result, isA&lt;Success&gt;());
    expect((result <span class="hljs-keyword">as</span> Success).value, equals(domain.PermissionStatus.granted));
  });

  test(<span class="hljs-string">'requestPermission returns Success with denied status'</span>, () <span class="hljs-keyword">async</span> {
    methodChannelMock = MethodChannelMock(
      channelName: <span class="hljs-string">'flutter.baseflow.com/permissions/methods'</span>,
      method: <span class="hljs-string">'requestPermissions'</span>,
      result: {<span class="hljs-number">0</span>: <span class="hljs-number">0</span>}, <span class="hljs-comment">// This represents PermissionStatus.denied</span>
    );

    <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> service.requestPermission(PermissionType.camera);

    expect(result, isA&lt;Success&gt;());
    expect((result <span class="hljs-keyword">as</span> Success).value, equals(domain.PermissionStatus.denied));
  });

  test(<span class="hljs-string">'requestPermission returns Failure on exception'</span>, () <span class="hljs-keyword">async</span> {
    methodChannelMock = MethodChannelMock(
      channelName: <span class="hljs-string">'flutter.baseflow.com/permissions/methods'</span>,
      method: <span class="hljs-string">'requestPermissions'</span>,
      result: PlatformException(code: <span class="hljs-string">'TEST_ERROR'</span>),
    );

    <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> service.requestPermission(PermissionType.camera);

    expect(result, isA&lt;Failure&gt;());
    expect((result <span class="hljs-keyword">as</span> Failure).error.code, equals(PermissionErrorCode.unknown));
  });
}
</code></pre>
<p>In these tests, we're using the MethodChannelMock to simulate different responses from the platform channel. Here's what's happening:</p>
<ol>
<li><p>We create a new MethodChannelMock in the setUp function with a default "granted" response.</p>
</li>
<li><p>In each test, we can create a new MethodChannelMock with different results to test various scenarios.</p>
</li>
<li><p>We test for granted permissions, denied permissions, and exceptions.</p>
</li>
</ol>
<h2 id="heading-advantages-of-using-methodchannelmock"><strong>Advantages of Using MethodChannelMock</strong></h2>
<p>The MethodChannelMock class provides several benefits:</p>
<ul>
<li><p><strong>Simplicity</strong>: It's easy to set up and use in tests, requiring minimal boilerplate code.</p>
</li>
<li><p><strong>Flexibility</strong>: You can easily change the result for different test scenarios.</p>
</li>
<li><p><strong>Realism</strong>: It closely mimics the actual method channel behavior, including the ability to simulate delays and exceptions.</p>
</li>
</ul>
<h2 id="heading-testing-different-permission-statuses"><strong>Testing Different Permission Statuses</strong></h2>
<p>To thoroughly test your permission service, create tests for all possible permission statuses.</p>
<p>The MethodChannelMock makes it easy to simulate these different outcomes:</p>
<pre><code class="lang-dart">test(<span class="hljs-string">'requestPermission returns Success with restricted status'</span>, () <span class="hljs-keyword">async</span> {
  methodChannelMock = MethodChannelMock(
    channelName: <span class="hljs-string">'flutter.baseflow.com/permissions/methods'</span>,
    method: <span class="hljs-string">'requestPermissions'</span>,
    result: {<span class="hljs-number">0</span>: <span class="hljs-number">1</span>}, <span class="hljs-comment">// This represents PermissionStatus.restricted</span>
  );

  <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> service.requestPermission(PermissionType.camera);

  expect(result, isA&lt;Success&gt;());
  expect((result <span class="hljs-keyword">as</span> Success).value, equals(domain.PermissionStatus.granted));
});
</code></pre>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Testing permission-handling classes in Flutter becomes more straightforward and reliable with a custom MethodChannelMock class. This approach allows you to:</p>
<ul>
<li><p>Simulate various permission statuses and error conditions.</p>
</li>
<li><p>Test your code's behavior without relying on actual device permissions.</p>
</li>
<li><p>Ensure your permission handling logic works correctly across different scenarios.</p>
</li>
</ul>
<p>By following this testing strategy, you can create comprehensive tests that cover various permission-related scenarios, leading to a more robust and reliable application.</p>
<p>Remember to test for all possible permission statuses, error conditions, and edge cases. This thorough testing will help you catch and fix issues early in the development process, ensuring your app handles permissions correctly in all situations.</p>
]]></content:encoded></item><item><title><![CDATA[Is Learning Flutter Worth it? My Year in Review.]]></title><description><![CDATA[Grab a glass of juice and follow me on a ride. I'm not sure how long this is going to be but it's worth mentioning that it might be a lengthy one.
Recommended
Listen to the Podcast version of this. I gave more detail on getting a Job as a Flutter Dev...]]></description><link>https://article.temiajiboye.com/is-learning-flutter-worth-it-my-year-in-review</link><guid isPermaLink="true">https://article.temiajiboye.com/is-learning-flutter-worth-it-my-year-in-review</guid><category><![CDATA[Flutter]]></category><category><![CDATA[community]]></category><category><![CDATA[learning]]></category><category><![CDATA[Learning Journey]]></category><category><![CDATA[interview]]></category><dc:creator><![CDATA[Temitope Ajiboye]]></dc:creator><pubDate>Wed, 29 Dec 2021 16:34:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/Gll-v69L8iA/upload/v1640772849369/Q0ODd6MuI.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab a glass of juice and follow me on a ride. I'm not sure how long this is going to be but it's worth mentioning that it might be a lengthy one.</p>
<h3 id="heading-recommended">Recommended</h3>
<p>Listen to the Podcast version of this. I gave more detail on getting a Job as a Flutter Developer</p>
<p> <a target="_blank" href="https://open.spotify.com/episode/2YujGK0GdaE1S9EAaCyCRC">https://open.spotify.com/episode/2YujGK0GdaE1S9EAaCyCRC</a> </p>
<h1 id="heading-about-me">About Me</h1>
<p>My name is Temitope and I am a <a target="_blank" href="https://flutter.dev">Flutter</a> Developer. I have been building mobile apps in the last 3 years using Flutter. Prior to that, I have been a Web developer building perhaps one of Nigeria's largest loan lending <a target="_blank" href="https://paywithspecta.com">platforms</a>. I eventually got tired of the web and decided to go head-on into mobile app development. </p>
<p>Was this a good decision? You will find out in a bit.</p>
<p>I have made a lot of progress as a developer and a major highlight of this is getting a job as a <strong>Flutter</strong> developer.</p>
<h1 id="heading-a-timeline">A Timeline</h1>
<p>Started the year with a plan - Learn as many Data Structures and Algorithms as possible - so I can confidently apply for Flutter jobs. I do not like to go to an interview unprepared and look like a fool. So January, I started learning and by the end of January, I was a little confident. I knew some C# and JavaScript so I used these two languages to practice some DS and Algo exercises.</p>
<p>The following month, February, I started applying to jobs on LinkedIn. </p>
<p>Applying to jobs on LinkedIn turned out to be a huge time waster. I am not sure how people get jobs from just applying to jobs on LinkedIn. Recruiters never got back to me on LinkedIn. </p>
<p>I think one would have a better chance by sending a message to recruiters directly on LinkedIn rather than wasting time using the job board. Another strategy that could work is to "<strong>Show workings</strong>".</p>
<h1 id="heading-show-workings">Show Workings</h1>
<p>"Rather than applying for jobs, why not get recruiters to notice you instead"? This was my thought after sending over 50+ applications on LinkedIn. But, how do I get them to notice me? </p>
<p>Show workings. </p>
<p>I believe that no matter how talented/genius you might be, hiding under the bed would make you go hungry and people won't see how good you really are. Or at most, you become underpaid. </p>
<p>I had worked at Sterling at the time and seen a lot of wasted talents in the banking industry getting underpaid because they did not show the world what they can do. </p>
<p>I had an idea. I will build pockets of Demos and POCs in Flutter and post them on Twitter and LinkedIn.
The first week I did that on LinkedIn, I had recruiters and CEOs in my Inbox.</p>
<p>It should be noted that you can see a better response from recruiters on Indeed than on LinkedIn when applying for jobs - my opinion.</p>
<blockquote>
<p>A city that is set on a hill cannot be hidden - Matt. 5:14</p>
</blockquote>
<h1 id="heading-a-stream-of-unfortunately">A Stream of "Unfortunately"</h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640781282227/gh09unKiw.png" alt="Screenshot at Dec 29 13-32-17.png" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640781291321/TIxnbeZAM.png" alt="Screenshot at Dec 29 13-32-47.png" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640781297579/30ZYotYUO.png" alt="Screenshot at Dec 29 13-33-47.png" /></p>
<p>Yeah! Typical. 
You apply for a job, go through some series of interviews and bam, you get those emails. Rough. It can mess with your self-esteem. </p>
<p>I was fortunate, I didn't take those emails seriously because I had a job at Sterling. If I didn't, it would have been really hard when those emails came in.</p>
<p>It was a learning process for me and the first thing I learnt was that recruitment for a Flutter role was quite different from what I was used to. I was expecting algorithm questions and the likes. </p>
<p>Imagine preparing a whole week solving challenges on Codility but then you get asked "<strong>What is the difference between a Stateless widget and Stateful widget</strong>". Hold up. I froze. An interview for another company, same series of questions. <em>confused</em></p>
<p>I was given a lot of take-home assessments too.</p>
<p>Then I figured, If I am going to land a job, I need to go learn the basics of Flutter in-depth. It was not like I didn't know how to write Flutter, I just never really paid attention to explaining what some concepts are. Some of those things are just big words - "immutability and mutability". Huh? It's simple right? Yes, but that is such a big word. Things that change and things that do not change. Simple.</p>
<p>This is a  <a target="_blank" href="https://twitter.com/passsy/status/1404903280728330245?s=20">thread</a>  on Twitter that helped me know which questions to expect in Flutter interviews.</p>
<p>Not that I'd advise it, I kept a folder for all the rejection emails. This helped me to reminisce and prepare for the next interview.</p>
<h1 id="heading-a-job-finally">A Job Finally</h1>
<p>Looks like it's usually the interview you thought you didn't do well in that you'd actually get a job with. </p>
<p>Got a job in October. Yay!</p>
<p>I had wanted to leave banking and fintech and so it's a dream come through. I remember getting an offer from a company in the UK. I turned down the offer because it is Fintech(<em>shhh.. na because of money sha. How I go dey work for company for UK dey collect local rate.</em>).</p>
<h1 id="heading-key-takeawayparty-jollof-inside">Key Takeaway(<em>Party Jollof inside</em>)</h1>
<p>I am not sure there is a key takeaway from what I have written so far, however, if this article was written by someone else and I was reading, my key takeaways would be:</p>
<ul>
<li>Applying for jobs on LinkedIn may be useless. You'd have more success with Indeed.</li>
<li>Showcase what you are capable of on LinkedIn and Twitter. They are powerful tools to gain exposure.</li>
<li>Job hunting is a game of numbers. </li>
</ul>
<p>Is learning Flutter worth it? Yes, it is. Apart from the joy I get to showcase the demos and POCs I built on Twitter and LinkedIn, I get paid as a Flutter Developer.</p>
<p>Will it be worth it if you learn Flutter? Definitely, yes.</p>
<p>In retrospect, I should have started applying for jobs earlier than I did. I thought I really needed DS and Algo in the interviews.</p>
<h1 id="heading-other-than-career">Other than career</h1>
<p>Moving away from career update, earlier this year, the  <a target="_blank" href="https://twitter.com/codeclannigeria">Code Clan Nigeria Community</a>, which I founded, moved to Discord. Since then we have grown to over 600+ members learning together. You can join us 
 <a target="_blank" href="https://twitter.com/codeclannigeria">here</a>.</p>
<p>By the way, if you are interested in volunteering for the community, please send me a DM on  <a target="_blank" href="https://twitter.com/codeclannigeria">Twitter</a>.</p>
<p>Earlier in the year, I also took on 3 Flutter Mentees. Next year, I will do the same.</p>
<h1 id="heading-life-update">Life Update</h1>
<p>Got engaged to the most beautiful woman in the world. This is the highlight of my 2021. </p>
<p>She loves her ring y'all.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640794404387/AepSM391s.jpeg" alt="IMG_0114.jpg" /></p>
<h1 id="heading-what-next">What next?</h1>
<ul>
<li>Next year, I will keep applying for jobs. I believe the time to apply for jobs is when you currently have a job. You'll be able to confidently do interviews and negotiate your pay.</li>
<li>Resume Podcasting. </li>
<li>Upload more videos to my YouTube  <a target="_blank" href="https://youtube.com/temicodes">channel</a>.</li>
<li>Launch a beginner's Flutter course.</li>
<li>Organize a meetup for Code Clan Nigeria.</li>
<li>Write more articles this year.</li>
</ul>
<p>Is this article long? Is this an article? Well, I hope it was a good read. </p>
<p>Your friend,</p>
<p>Temi(<em>not Timi</em>) Ajiboye</p>
]]></content:encoded></item><item><title><![CDATA[Code Clan Nigeria’s Year in Review 2020]]></title><description><![CDATA[Code Clan Nigeria’s Year in Review 2020


When we started out in 2020, the plan was to reiterate and implement our goals and the reasons why we started Code Clan Nigeria which is simple: Turn 500+ people into developers every 3 months.
By that goal, ...]]></description><link>https://article.temiajiboye.com/code-clan-nigerias-year-in-review-2020</link><guid isPermaLink="true">https://article.temiajiboye.com/code-clan-nigerias-year-in-review-2020</guid><category><![CDATA[community]]></category><dc:creator><![CDATA[Temitope Ajiboye]]></dc:creator><pubDate>Thu, 31 Dec 2020 11:22:35 GMT</pubDate><content:encoded><![CDATA[<p><span class="s"></span></p>
<h1 id="code-clan-nigerias-year-in-review-2020">Code Clan Nigeria’s Year in Review 2020</h1>
<img alt="Image for post" src="https://miro.medium.com/max/4964/1*UwxU_466MAAUtb8RPxrZPg.jpeg" />

<p><span class="s ib ic id ie if ig ih ii ij gn">W</span>hen we started out in 2020, the plan was to reiterate and implement our goals and the reasons why we started Code Clan Nigeria which is simple: <strong>Turn 500+ people into developers every 3 months</strong>.</p>
<p>By that goal, the aim was to turn 2000+ people into developers at the end of 2020.</p>
<blockquote>
<p><strong>“Alone, we can do so little; together, we can do so much” — Helen Keller</strong></p>
</blockquote>
<p>To achieve our goal, we had a 3-part plan:</p>
<ul>
<li>Empower community members with technical skills using mentorships.</li>
<li>Launch chapters in multiple cities in Nigeria.</li>
<li>Connect community members with job opportunities.</li>
</ul>
<p>Did we reach the goal? Let’s find out.</p>
<h1 id="overview">Overview</h1>
<p>Code Clan Nigeria was created by <a target="_blank" href="https://twitter.com/olu_tayormi">Ajiboye Temitope</a> on 28/12/2018. 2 years after, we have built a <a target="_blank" href="http://t.me/codeclannigeria">Telegram</a> community of 750 members, a Slack community of 579 folks and a <a target="_blank" href="https://www.meetup.com/CodeClanNigeria/">Meetup</a> group of 693 members where we have created over 10 events to improve our member’s technical skills.</p>
<p>The core of our community is to provide mentorships to newbie developers and in turn, they become mentors themselves after going through a Tech track. Equipped with the strategy, we started the year.</p>
<h1 id="january-april-2020">January — April 2020</h1>
<p>Since the core function of our community is mentorship, we started the year with the first mentorship of the year. This was supposedly supposed to run for 3 months but got extended to 4 months. We ran a single-track mentorship which was in Frontend because of the limited resources we had. In total, we had 13 mentors who completed the mentorship and they became mentors themselves.</p>
<h1 id="may-august-2020">May — August 2020</h1>
<p>With the outbreak of the Covid-19 virus, we re-imagined the way to run our mentorships while taking cues from the first mentorship of the year. If we are going to be turning newbies to developers and then to mentors, we would need a portal that will bring them together and automate the bulk of the processing. <em>Imagine a portal where a newbie enrols on then get matched with a mentor immediately.</em> With this in mind, we set out to build out community portal with the amazing mentees that we had mentored the first year. This gave birth to our mentorship platform on <a target="_blank" href="http://www.codeclannigeria.dev">www.codeclannigeria.dev</a>. You can take a look at the wonderful people that made this happen <a target="_blank" href="http://www.codeclannigeria.dev/team">here</a>.</p>
<p>Additionally, we ran bootcamps to teach people skills doing this period. An example of such event can be found <a target="_blank" href="https://ccnreactbootcamp.splashthat.com/">here</a>.</p>
<p><img src="https://miro.medium.com/max/60/1*kRTFyCESm8X5CmDdyq1EPg.png?q=20" alt="Image for post" /></p>
<img alt="Image for post" src="https://miro.medium.com/max/4476/1*kRTFyCESm8X5CmDdyq1EPg.png" />

<p>This was our first ever 3-day bootcamp and we amazingly saw 80+ enrollment for this event.</p>
<p><img src="https://miro.medium.com/max/60/1*Yw3lGxfvTQ9ACrh5OhlERw.png?q=20" alt="Image for post" /></p>
<img alt="Image for post" src="https://miro.medium.com/max/1152/1*Yw3lGxfvTQ9ACrh5OhlERw.png" />

<p>We started #codeclassSaturdays during this period as well. Code class saturdays is a weekly tutorial class tailored to beginners. The classes are intended to bring everyone together to learn together. We created these events on our Meetup group. We started with classes in Javascript and then we added Flutter to the mix.</p>
<p><img src="https://miro.medium.com/max/60/1*K5I8pX5wlzvn6r1M8cx-rA.png?q=20" alt="Image for post" /></p>
<img alt="Image for post" src="https://miro.medium.com/max/3024/1*K5I8pX5wlzvn6r1M8cx-rA.png" />

<p>code class saturdays images</p>
<p>On Monday, August 24th, we launched our mentorship portal with a launch party. A lot of our mentees loved the idea. We had over 100 enrollments the first day of launch. You can find the launch party video <a target="_blank" href="https://www.youtube.com/watch?v=90L-vSQpC-k">here</a>.</p>
<h1 id="september-december-2020">September — December 2020</h1>
<p>During this period, hmm, a lot of things went wrong. We had a lot of problems we did not anticipate when building the portal and so, although we had a lot of enrollments, we ran into several problems which made it difficult for mentees to get mentored properly. It took a month of intense work to make the portal stable and by the time it was stable, a lot of our mentees were already exhausted and left. We learnt a huge lesson. Thankfully, these lesson made us improve and change the dynamics of how we deliver mentorships in 2021 and in future.</p>
<p>To test our theory, we launch a mini 4-weeks Flutter Mentorship track which saw over 30 people enroll. The positive engagement and response we got meant that we finally cracked the way to do mentorship.</p>
<blockquote>
<p><strong>“For the things we have to learn before we can do them, we learn by doing them.” — Aristole</strong></p>
</blockquote>
<h1 id="the-positives">The Positives</h1>
<p>Asides the numerous negatives, we had matching positives as well which gives a hope of a better 2021.</p>
<p>We had 347 enrollments on the portal which means, we have been able to touch 347 lives in 2020.</p>
<p><img src="https://miro.medium.com/max/60/1*ZBi6Wr-N1B3o2E0X8fd9qg.png?q=20" alt="Image for post" /></p>
<img alt="Image for post" src="https://miro.medium.com/max/4196/1*ZBi6Wr-N1B3o2E0X8fd9qg.png" />

<p><strong>Podcast</strong></p>
<p>Our Podcast on <a target="_blank" href="https://anchor.fm/codeclanpodcast">Code Clan Nigeria Podcast</a> where we interview budding developers has listeners in 35+ countries alone this year with Nigeria and the United States topping the list.</p>
<ul>
<li>5 episodes</li>
<li>10 listening platform</li>
<li>Most listen: <a target="_blank" href="https://anchor.fm/codeclanpodcast/episodes/The-Student-Pharmacist-and-a-UIUX-Designer-Alter-Ego-ejp7cc">https://anchor.fm/codeclanpodcast/episodes/The-Student-Pharmacist-and-a-UIUX-Designer-Alter-Ego-ejp7cc</a></li>
</ul>
<p>Listen to our episodes here: <a target="_blank" href="https://anchor.fm/codeclanpodcast">https://anchor.fm/codeclanpodcast</a></p>
<p><img src="https://miro.medium.com/max/60/1*9NCACxYJWroiiJC888HkDw.png?q=20" alt="Image for post" /></p>
<img alt="Image for post" src="https://miro.medium.com/max/4008/1*9NCACxYJWroiiJC888HkDw.png" />

<p><strong>Newsletter</strong></p>
<p>We started an <a target="_blank" href="https://codeclannigeria.substack.com/">official newsletter</a> — a weekly newsletter that keep the community updated on happenings in the tech world and also to motivate.</p>
<ul>
<li>4 newsletters</li>
<li>400 subscribers</li>
</ul>
<p>Most read newletter this year: <a target="_blank" href="https://codeclannigeria.substack.com/p/why-do-i-feel-so-weak">https://codeclannigeria.substack.com/p/why-do-i-feel-so-weak</a></p>
<p>Subcribe to our newsletter: <a target="_blank" href="https://codeclannigeria.substack.com/p/why-do-i-feel-so-weak">https://codeclannigeria.substack.com/</a></p>
<p>Thanks to our Newsletter editor for an all year commitment and dedication: <a target="_blank" href="https://twitter.com/codewithkisha">Jennifer Oluchi Ofordile</a> which also doubles as our Social Media Team lead.</p>
<p><strong>Structure</strong></p>
<p>As part of revamping, we set out to resturcture the administration in November. We carefully identified the needs of the community and appointed leads over each section of the community.</p>
<h1 id="plans-for-2021">Plans for 2021?</h1>
<ul>
<li>We are going to 2021 stronger than we have ever been. Armed with the lessons of 2020, we with run the mentorships smoothly, increase enrollments and reduce dropoffs.</li>
<li>We also plan to hold our first ever Meetup in 2021 — the aim is to connect, give newbies speaking opportunities and to allow employers connect with our ever growing mentees who have learnt technical skills.</li>
<li>Expand the community by creating clan in multiple cities. We will start with 1 and expand exponentially.</li>
<li>Create impactful programs to further benefit our members.</li>
<li>Connect special talented mentees with companies who need their skills. We are adding a job pipeline.</li>
</ul>
<h1 id="sponsorship">Sponsorship</h1>
<p>Without money, we cannot run effectively. We need funds to run our planned activities, our community leads are just passionate folks who give their time and money to discover talents.</p>
<p>Send us an email at <strong>codeclannigeria@gmail.com</strong> if you want to sponsor Code Clan Nigeria, either as an individual or a company.</p>
<p>To learn more about Code Clan Nigeria, our programs and initiatives, you can visit <a target="_blank" href="http://www.codeclannigeria.dev">www.codeclannigeria.dev</a> or send us a mail at codeclannigeria@gmail.com</p>
<p>A special thanks to the Code Clan Nigeria team leads for their tremendous help in 2020.</p>
<p>You can find the team list here: <a target="_blank" href="http://www.codeclannigeria.dev/team">www.codeclannigeria.dev/team</a></p>
]]></content:encoded></item></channel></rss>